Refactor VA test to use Go's httptest.
Previously the VA test had race conditions where the various test servers would not shut down before the next test started its own server, and the necessary port wouldn't be available. Go's httptest makes shutdown simpler, and also chooses a random port, which further helps avoid collisions. This change required refactoring the VA to specify the ports for various challenges as fields. This should allow us to fully remove the TestMode bool in a subsequent change. Credit to jmhodges for the first version of this patch.
This commit is contained in:
parent
803193a19e
commit
efa94628c7
|
|
@ -40,12 +40,14 @@ var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
|
|||
|
||||
// ValidationAuthorityImpl represents a VA
|
||||
type ValidationAuthorityImpl struct {
|
||||
RA core.RegistrationAuthority
|
||||
log *blog.AuditLogger
|
||||
DNSResolver core.DNSResolver
|
||||
IssuerDomain string
|
||||
TestMode bool
|
||||
UserAgent string
|
||||
RA core.RegistrationAuthority
|
||||
log *blog.AuditLogger
|
||||
DNSResolver core.DNSResolver
|
||||
IssuerDomain string
|
||||
simpleHTTPPort int
|
||||
simpleHTTPSPort int
|
||||
dvsniPort int
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// NewValidationAuthorityImpl constructs a new VA, and may place it
|
||||
|
|
@ -53,7 +55,23 @@ type ValidationAuthorityImpl struct {
|
|||
func NewValidationAuthorityImpl(tm bool) ValidationAuthorityImpl {
|
||||
logger := blog.GetAuditLogger()
|
||||
logger.Notice("Validation Authority Starting")
|
||||
return ValidationAuthorityImpl{log: logger, TestMode: tm}
|
||||
// TODO(jsha): Remove TestMode entirely. Instead, the various validation ports
|
||||
// should be exported, so the cmd file can set them based on a config.
|
||||
if tm {
|
||||
return ValidationAuthorityImpl{
|
||||
log: logger,
|
||||
simpleHTTPPort: 5001,
|
||||
simpleHTTPSPort: 5001,
|
||||
dvsniPort: 5001,
|
||||
}
|
||||
} else {
|
||||
return ValidationAuthorityImpl{
|
||||
log: logger,
|
||||
simpleHTTPPort: 80,
|
||||
simpleHTTPSPort: 443,
|
||||
dvsniPort: 443,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used for audit logging
|
||||
|
|
@ -155,10 +173,8 @@ func (d *dialer) Dial(_, _ string) (net.Conn, error) {
|
|||
// resolveAndConstructDialer gets the prefered address using va.getAddr and returns
|
||||
// the chosen address and dialer for that address and correct port.
|
||||
func (va ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort string) (dialer, *core.ProblemDetails) {
|
||||
port := "80"
|
||||
if va.TestMode {
|
||||
port = "5001"
|
||||
} else if defaultPort != "" {
|
||||
port := fmt.Sprintf("%d", va.simpleHTTPPort)
|
||||
if defaultPort != "" {
|
||||
port = defaultPort
|
||||
}
|
||||
d := dialer{
|
||||
|
|
@ -192,18 +208,23 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
|
|||
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] Identifier failure", identifier))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
hostName := identifier.Value
|
||||
|
||||
host := identifier.Value
|
||||
var scheme string
|
||||
var port int
|
||||
if input.TLS == nil || (input.TLS != nil && *input.TLS) {
|
||||
scheme = "https"
|
||||
port = va.simpleHTTPSPort
|
||||
} else {
|
||||
scheme = "http"
|
||||
port = va.simpleHTTPPort
|
||||
}
|
||||
portString := fmt.Sprintf("%d", port)
|
||||
hostPort := net.JoinHostPort(host, portString)
|
||||
|
||||
url := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostName,
|
||||
Host: hostPort,
|
||||
Path: fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token),
|
||||
}
|
||||
|
||||
|
|
@ -224,12 +245,8 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
|
|||
httpRequest.Header["User-Agent"] = []string{va.UserAgent}
|
||||
}
|
||||
|
||||
httpRequest.Host = hostName
|
||||
var port string
|
||||
if scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
dialer, prob := va.resolveAndConstructDialer(hostName, port)
|
||||
httpRequest.Host = hostPort
|
||||
dialer, prob := va.resolveAndConstructDialer(host, portString)
|
||||
dialer.record.URL = url.String()
|
||||
challenge.ValidationRecord = append(challenge.ValidationRecord, dialer.record)
|
||||
if prob != nil {
|
||||
|
|
@ -255,15 +272,15 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
|
|||
return fmt.Errorf("Too many redirects")
|
||||
}
|
||||
|
||||
host := req.URL.Host
|
||||
port = ""
|
||||
if strings.Contains(host, ":") {
|
||||
splitHost := strings.SplitN(host, ":", 2)
|
||||
reqHost := req.URL.Host
|
||||
reqPort := ""
|
||||
if strings.Contains(reqHost, ":") {
|
||||
splitHost := strings.SplitN(reqHost, ":", 2)
|
||||
if len(splitHost) <= 1 {
|
||||
return fmt.Errorf("Malformed host")
|
||||
}
|
||||
host, port = splitHost[0], splitHost[1]
|
||||
portNum, err := strconv.Atoi(port)
|
||||
reqHost, reqPort = splitHost[0], splitHost[1]
|
||||
portNum, err := strconv.Atoi(reqPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -271,10 +288,10 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
|
|||
return fmt.Errorf("Invalid port number in redirect")
|
||||
}
|
||||
} else if strings.ToLower(req.URL.Scheme) == "https" {
|
||||
port = "443"
|
||||
reqPort = "443"
|
||||
}
|
||||
|
||||
dialer, err := va.resolveAndConstructDialer(host, port)
|
||||
dialer, err := va.resolveAndConstructDialer(reqHost, reqPort)
|
||||
dialer.record.URL = req.URL.String()
|
||||
challenge.ValidationRecord = append(challenge.ValidationRecord, dialer.record)
|
||||
if err != nil {
|
||||
|
|
@ -412,12 +429,9 @@ func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
|
|||
}
|
||||
|
||||
// Make a connection with SNI = nonceName
|
||||
hostPort := net.JoinHostPort(addr.String(), "443")
|
||||
challenge.ValidationRecord[0].Port = "443"
|
||||
if va.TestMode {
|
||||
hostPort = net.JoinHostPort(addr.String(), "5001")
|
||||
challenge.ValidationRecord[0].Port = "5001"
|
||||
}
|
||||
portString := fmt.Sprintf("%d", va.dvsniPort)
|
||||
hostPort := net.JoinHostPort(addr.String(), portString)
|
||||
challenge.ValidationRecord[0].Port = portString
|
||||
va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s",
|
||||
identifier, hostPort, ZName))
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,604 @@
|
|||
// Copyright 2014 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package va
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/policy"
|
||||
)
|
||||
|
||||
const maxCNAME = 16 // Prevents infinite loops. Same limit as BIND.
|
||||
|
||||
// Returned by CheckCAARecords if it has to follow too many
|
||||
// consecutive CNAME lookups.
|
||||
var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
|
||||
|
||||
// ValidationAuthorityImpl represents a VA
|
||||
type ValidationAuthorityImpl struct {
|
||||
RA core.RegistrationAuthority
|
||||
log *blog.AuditLogger
|
||||
DNSResolver core.DNSResolver
|
||||
IssuerDomain string
|
||||
TestMode bool
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// NewValidationAuthorityImpl constructs a new VA, and may place it
|
||||
// into Test Mode (tm)
|
||||
func NewValidationAuthorityImpl(tm bool) ValidationAuthorityImpl {
|
||||
logger := blog.GetAuditLogger()
|
||||
logger.Notice("Validation Authority Starting")
|
||||
return ValidationAuthorityImpl{log: logger, TestMode: tm}
|
||||
}
|
||||
|
||||
// Used for audit logging
|
||||
type verificationRequestEvent struct {
|
||||
ID string `json:",omitempty"`
|
||||
Requester int64 `json:",omitempty"`
|
||||
Challenge core.Challenge `json:",omitempty"`
|
||||
RequestTime time.Time `json:",omitempty"`
|
||||
ResponseTime time.Time `json:",omitempty"`
|
||||
Error string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.JsonWebKey, target map[string]interface{}) error {
|
||||
|
||||
if len(validation.Signatures) > 1 {
|
||||
return fmt.Errorf("Too many signatures on validation JWS")
|
||||
}
|
||||
if len(validation.Signatures) == 0 {
|
||||
return fmt.Errorf("Validation JWS not signed")
|
||||
}
|
||||
|
||||
payload, _, err := validation.Verify(accountKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Validation JWS failed to verify: %s", err.Error())
|
||||
}
|
||||
|
||||
var parsedResponse map[string]interface{}
|
||||
err = json.Unmarshal(payload, &parsedResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Validation payload failed to parse as JSON: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(parsedResponse) != len(target) {
|
||||
return fmt.Errorf("Validation payload had an improper number of fields")
|
||||
}
|
||||
|
||||
for key, targetValue := range target {
|
||||
parsedValue, ok := parsedResponse[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("Validation payload missing a field %s", key)
|
||||
} else if parsedValue != targetValue {
|
||||
return fmt.Errorf("Validation payload has improper value for field %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validation methods
|
||||
|
||||
// setChallengeErrorFromDNSError checks the error returned from Lookup...
|
||||
// methods and tests if the error was an underlying net.OpError or an error
|
||||
// caused by resolver returning SERVFAIL or other invalid Rcodes and sets
|
||||
// the challenge.Error field accordingly.
|
||||
func setChallengeErrorFromDNSError(err error, challenge *core.Challenge) {
|
||||
challenge.Error = &core.ProblemDetails{Type: core.ConnectionProblem}
|
||||
if netErr, ok := err.(*net.OpError); ok {
|
||||
if netErr.Timeout() {
|
||||
challenge.Error.Detail = "DNS query timed out"
|
||||
} else if netErr.Temporary() {
|
||||
challenge.Error.Detail = "Temporary network connectivity error"
|
||||
}
|
||||
} else {
|
||||
challenge.Error.Detail = "Server failure at resolver"
|
||||
}
|
||||
}
|
||||
|
||||
func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) {
|
||||
challenge := input
|
||||
|
||||
if identifier.Type != core.IdentifierDNS {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.MalformedProblem,
|
||||
Detail: "Identifier type for SimpleHTTP was not DNS",
|
||||
}
|
||||
|
||||
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] Identifier failure", identifier))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
hostName := identifier.Value
|
||||
|
||||
var scheme string
|
||||
if input.TLS == nil || (input.TLS != nil && *input.TLS) {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
if va.TestMode {
|
||||
hostName = "localhost:5001"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s://%s/.well-known/acme-challenge/%s", scheme, hostName, challenge.Token)
|
||||
|
||||
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
|
||||
va.log.Audit(fmt.Sprintf("Attempting to validate Simple%s for %s", strings.ToUpper(scheme), url))
|
||||
httpRequest, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.MalformedProblem,
|
||||
Detail: "URL provided for SimpleHTTP was invalid",
|
||||
}
|
||||
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] HTTP failure: %s", identifier, err))
|
||||
challenge.Status = core.StatusInvalid
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
if va.UserAgent != "" {
|
||||
httpRequest.Header["User-Agent"] = []string{va.UserAgent}
|
||||
}
|
||||
|
||||
httpRequest.Host = hostName
|
||||
tr := &http.Transport{
|
||||
// We are talking to a client that does not yet have a certificate,
|
||||
// so we accept a temporary, invalid one.
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
// We don't expect to make multiple requests to a client, so close
|
||||
// connection immediately.
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
logRedirect := func(req *http.Request, via []*http.Request) error {
|
||||
va.log.Info(fmt.Sprintf("validateSimpleHTTP [%s] redirect from %q to %q", identifier, via[len(via)-1].URL.String(), req.URL.String()))
|
||||
return nil
|
||||
}
|
||||
client := http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: logRedirect,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
|
||||
if err != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: parseHTTPConnError(err),
|
||||
Detail: fmt.Sprintf("Could not connect to %s", url),
|
||||
}
|
||||
va.log.Debug(strings.Join([]string{challenge.Error.Error(), err.Error()}, ": "))
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: fmt.Sprintf("Invalid response from %s: %d",
|
||||
url, httpResponse.StatusCode),
|
||||
}
|
||||
err = challenge.Error
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
// Read body & test
|
||||
body, readErr := ioutil.ReadAll(httpResponse.Body)
|
||||
if readErr != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: fmt.Sprintf("Error reading HTTP response body"),
|
||||
}
|
||||
return challenge, readErr
|
||||
}
|
||||
|
||||
// Parse and verify JWS
|
||||
parsedJws, err := jose.ParseSigned(string(body))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Validation response failed to parse as JWS: %s", err.Error())
|
||||
va.log.Debug(err.Error())
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
// Check that JWS body is as expected
|
||||
// * "type" == "simpleHttp"
|
||||
// * "token" == challenge.token
|
||||
// * "tls" == challenge.tls || true
|
||||
target := map[string]interface{}{
|
||||
"type": core.ChallengeTypeSimpleHTTP,
|
||||
"token": challenge.Token,
|
||||
"tls": (challenge.TLS == nil) || *challenge.TLS,
|
||||
}
|
||||
err = verifyValidationJWS(parsedJws, &accountKey, target)
|
||||
if err != nil {
|
||||
va.log.Debug(err.Error())
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
challenge.Status = core.StatusValid
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) {
|
||||
challenge := input
|
||||
|
||||
if identifier.Type != "dns" {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.MalformedProblem,
|
||||
Detail: "Identifier type for DVSNI was not DNS",
|
||||
}
|
||||
challenge.Status = core.StatusInvalid
|
||||
va.log.Debug(fmt.Sprintf("DVSNI [%s] Identifier failure", identifier))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
// Check that JWS body is as expected
|
||||
// * "type" == "dvsni"
|
||||
// * "token" == challenge.token
|
||||
target := map[string]interface{}{
|
||||
"type": core.ChallengeTypeDVSNI,
|
||||
"token": challenge.Token,
|
||||
}
|
||||
err := verifyValidationJWS((*jose.JsonWebSignature)(challenge.Validation), &accountKey, target)
|
||||
if err != nil {
|
||||
va.log.Debug(err.Error())
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
return challenge, err
|
||||
}
|
||||
|
||||
// Compute the digest that will appear in the certificate
|
||||
encodedSignature := core.B64enc(challenge.Validation.Signatures[0].Signature)
|
||||
h := sha256.New()
|
||||
h.Write([]byte(encodedSignature))
|
||||
Z := hex.EncodeToString(h.Sum(nil))
|
||||
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.DVSNISuffix)
|
||||
|
||||
// Make a connection with SNI = nonceName
|
||||
hostPort := identifier.Value + ":443"
|
||||
if va.TestMode {
|
||||
hostPort = "localhost:5001"
|
||||
}
|
||||
va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s",
|
||||
identifier, hostPort, ZName))
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", hostPort, &tls.Config{
|
||||
ServerName: ZName,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: parseHTTPConnError(err),
|
||||
Detail: "Failed to connect to host for DVSNI challenge",
|
||||
}
|
||||
va.log.Debug(fmt.Sprintf("DVSNI [%s] TLS Connection failure: %s", identifier, err))
|
||||
return challenge, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Check that ZName is a dNSName SAN in the server's certificate
|
||||
certs := conn.ConnectionState().PeerCertificates
|
||||
if len(certs) == 0 {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: "No certs presented for DVSNI challenge",
|
||||
}
|
||||
challenge.Status = core.StatusInvalid
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
for _, name := range certs[0].DNSNames {
|
||||
if subtle.ConstantTimeCompare([]byte(name), []byte(ZName)) == 1 {
|
||||
challenge.Status = core.StatusValid
|
||||
return challenge, nil
|
||||
}
|
||||
}
|
||||
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: "Correct ZName not found for DVSNI challenge",
|
||||
}
|
||||
challenge.Status = core.StatusInvalid
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
// parseHTTPConnError returns the ACME ProblemType corresponding to an error
|
||||
// that occurred during domain validation.
|
||||
func parseHTTPConnError(err error) core.ProblemType {
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
err = urlErr.Err
|
||||
}
|
||||
|
||||
// XXX: On all of the resolvers I tested that validate DNSSEC, there is
|
||||
// no differentation between a DNSSEC failure and an unknown host. If we
|
||||
// do not verify DNSSEC ourselves, this function should be modified.
|
||||
if netErr, ok := err.(*net.OpError); ok {
|
||||
dnsErr, ok := netErr.Err.(*net.DNSError)
|
||||
if ok && !dnsErr.Timeout() && !dnsErr.Temporary() {
|
||||
return core.UnknownHostProblem
|
||||
} else if fmt.Sprintf("%T", netErr.Err) == "tls.alert" {
|
||||
return core.TLSProblem
|
||||
}
|
||||
}
|
||||
|
||||
return core.ConnectionProblem
|
||||
}
|
||||
|
||||
func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) {
|
||||
challenge := input
|
||||
|
||||
if identifier.Type != core.IdentifierDNS {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.MalformedProblem,
|
||||
Detail: "Identifier type for DNS was not itself DNS",
|
||||
}
|
||||
va.log.Debug(fmt.Sprintf("DNS [%s] Identifier failure", identifier))
|
||||
challenge.Status = core.StatusInvalid
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
// Check that JWS body is as expected
|
||||
// * "type" == "dvsni"
|
||||
// * "token" == challenge.token
|
||||
target := map[string]interface{}{
|
||||
"type": core.ChallengeTypeDNS,
|
||||
"token": challenge.Token,
|
||||
}
|
||||
err := verifyValidationJWS((*jose.JsonWebSignature)(challenge.Validation), &accountKey, target)
|
||||
if err != nil {
|
||||
va.log.Debug(err.Error())
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
return challenge, err
|
||||
}
|
||||
encodedSignature := core.B64enc(challenge.Validation.Signatures[0].Signature)
|
||||
|
||||
// Look for the required record in the DNS
|
||||
challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, identifier.Value)
|
||||
txts, _, err := va.DNSResolver.LookupTXT(challengeSubdomain)
|
||||
|
||||
if err != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
setChallengeErrorFromDNSError(err, &challenge)
|
||||
va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
for _, element := range txts {
|
||||
if subtle.ConstantTimeCompare([]byte(element), []byte(encodedSignature)) == 1 {
|
||||
challenge.Status = core.StatusValid
|
||||
return challenge, nil
|
||||
}
|
||||
}
|
||||
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.UnauthorizedProblem,
|
||||
Detail: "Correct value not found for DNS challenge",
|
||||
}
|
||||
challenge.Status = core.StatusInvalid
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
// Overall validation process
|
||||
|
||||
func (va ValidationAuthorityImpl) validate(authz core.Authorization, challengeIndex int, accountKey jose.JsonWebKey) {
|
||||
|
||||
// Select the first supported validation method
|
||||
// XXX: Remove the "break" lines to process all supported validations
|
||||
logEvent := verificationRequestEvent{
|
||||
ID: authz.ID,
|
||||
Requester: authz.RegistrationID,
|
||||
RequestTime: time.Now(),
|
||||
}
|
||||
if !authz.Challenges[challengeIndex].IsSane(true) {
|
||||
chall := &authz.Challenges[challengeIndex]
|
||||
chall.Status = core.StatusInvalid
|
||||
chall.Error = &core.ProblemDetails{Type: core.MalformedProblem,
|
||||
Detail: fmt.Sprintf("Challenge failed sanity check.")}
|
||||
logEvent.Challenge = *chall
|
||||
logEvent.Error = chall.Error.Detail
|
||||
} else {
|
||||
var err error
|
||||
|
||||
switch authz.Challenges[challengeIndex].Type {
|
||||
case core.ChallengeTypeSimpleHTTP:
|
||||
authz.Challenges[challengeIndex], err = va.validateSimpleHTTP(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
|
||||
break
|
||||
case core.ChallengeTypeDVSNI:
|
||||
authz.Challenges[challengeIndex], err = va.validateDvsni(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
|
||||
break
|
||||
case core.ChallengeTypeDNS:
|
||||
authz.Challenges[challengeIndex], err = va.validateDNS(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
|
||||
break
|
||||
}
|
||||
|
||||
logEvent.Challenge = authz.Challenges[challengeIndex]
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
|
||||
va.log.AuditObject("Validation result", logEvent)
|
||||
|
||||
va.log.Notice(fmt.Sprintf("Validations: %+v", authz))
|
||||
|
||||
va.RA.OnValidationUpdate(authz)
|
||||
}
|
||||
|
||||
// UpdateValidations runs the validate() method asynchronously using goroutines.
|
||||
func (va ValidationAuthorityImpl) UpdateValidations(authz core.Authorization, challengeIndex int, accountKey jose.JsonWebKey) error {
|
||||
go va.validate(authz, challengeIndex, accountKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CAASet consists of filtered CAA records
|
||||
type CAASet struct {
|
||||
Issue []*dns.CAA
|
||||
Issuewild []*dns.CAA
|
||||
Iodef []*dns.CAA
|
||||
Unknown []*dns.CAA
|
||||
}
|
||||
|
||||
// returns true if any CAA records have unknown tag properties and are flagged critical.
|
||||
func (caaSet CAASet) criticalUnknown() bool {
|
||||
if len(caaSet.Unknown) > 0 {
|
||||
for _, caaRecord := range caaSet.Unknown {
|
||||
// Critical flag is 1, but according to RFC 6844 any flag other than
|
||||
// 0 should currently be interpreted as critical.
|
||||
if caaRecord.Flag > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter CAA records by property
|
||||
func newCAASet(CAAs []*dns.CAA) *CAASet {
|
||||
var filtered CAASet
|
||||
|
||||
for _, caaRecord := range CAAs {
|
||||
switch caaRecord.Tag {
|
||||
case "issue":
|
||||
filtered.Issue = append(filtered.Issue, caaRecord)
|
||||
case "issuewild":
|
||||
filtered.Issuewild = append(filtered.Issuewild, caaRecord)
|
||||
case "iodef":
|
||||
filtered.Iodef = append(filtered.Iodef, caaRecord)
|
||||
default:
|
||||
filtered.Unknown = append(filtered.Unknown, caaRecord)
|
||||
}
|
||||
}
|
||||
|
||||
return &filtered
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) getCAASet(hostname string) (*CAASet, error) {
|
||||
label := strings.TrimRight(hostname, ".")
|
||||
cnames := 0
|
||||
// See RFC 6844 "Certification Authority Processing" for pseudocode.
|
||||
for {
|
||||
if strings.IndexRune(label, '.') == -1 {
|
||||
// Reached TLD
|
||||
break
|
||||
}
|
||||
if _, present := policy.PublicSuffixList[label]; present {
|
||||
break
|
||||
}
|
||||
CAAs, _, err := va.DNSResolver.LookupCAA(label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(CAAs) > 0 {
|
||||
return newCAASet(CAAs), nil
|
||||
}
|
||||
cname, _, err := va.DNSResolver.LookupCNAME(label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dname, _, err := va.DNSResolver.LookupDNAME(label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cname == "" && dname == "" {
|
||||
// Try parent domain (note we confirmed
|
||||
// earlier that label contains '.')
|
||||
label = label[strings.IndexRune(label, '.')+1:]
|
||||
continue
|
||||
}
|
||||
if cname != "" && dname != "" && cname != dname {
|
||||
return nil, errors.New("both CNAME and DNAME exist for " + label)
|
||||
}
|
||||
if cname != "" {
|
||||
label = cname
|
||||
} else {
|
||||
label = dname
|
||||
}
|
||||
if cnames++; cnames > maxCNAME {
|
||||
return nil, ErrTooManyCNAME
|
||||
}
|
||||
}
|
||||
// no CAA records found
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CheckCAARecords verifies that, if the indicated subscriber domain has any CAA
|
||||
// records, they authorize the configured CA domain to issue a certificate
|
||||
func (va *ValidationAuthorityImpl) CheckCAARecords(identifier core.AcmeIdentifier) (present, valid bool, err error) {
|
||||
hostname := strings.ToLower(identifier.Value)
|
||||
caaSet, err := va.getCAASet(hostname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if caaSet == nil {
|
||||
// No CAA records found, can issue
|
||||
present = false
|
||||
valid = true
|
||||
return
|
||||
} else if caaSet.criticalUnknown() {
|
||||
present = true
|
||||
valid = false
|
||||
return
|
||||
} else if len(caaSet.Issue) > 0 || len(caaSet.Issuewild) > 0 {
|
||||
present = true
|
||||
var checkSet []*dns.CAA
|
||||
if strings.SplitN(hostname, ".", 2)[0] == "*" {
|
||||
checkSet = caaSet.Issuewild
|
||||
} else {
|
||||
checkSet = caaSet.Issue
|
||||
}
|
||||
for _, caa := range checkSet {
|
||||
if caa.Value == va.IssuerDomain {
|
||||
valid = true
|
||||
return
|
||||
} else if caa.Flag > 0 {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
--- va/validation-authority.go
|
||||
+++ va/validation-authority.go
|
||||
@@ -229,12 +228,34 @@ func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
|
||||
zName := fmt.Sprintf("%064x.acme.invalid", z)
|
||||
|
||||
// Make a connection with SNI = nonceName
|
||||
- hostPort := identifier.Value + ":443"
|
||||
- if va.TestMode {
|
||||
- hostPort = "localhost:5001"
|
||||
+ host, port, err := net.SplitHostPort(identifier.Value)
|
||||
+ if err != nil {
|
||||
+ if aerr, ok := err.(*net.AddrError); ok && aerr.Err == "missing port in address" {
|
||||
+ host = identifier.Value
|
||||
+ port = "443"
|
||||
+ } else {
|
||||
+ return challenge, err
|
||||
+ }
|
||||
}
|
||||
va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s",
|
||||
- identifier, hostPort, zName))
|
||||
+ identifier, identifier.Value, zName))
|
||||
+ fmt.Fprintf(os.Stderr, "va uh %#v %#v\n", host, identifier.Value)
|
||||
+ ips, _, _, err := va.DNSResolver.LookupHost(host)
|
||||
+ // FIXME i'm assuming ips has at least one address if err is nil
|
||||
+ if err != nil {
|
||||
+ challenge.Status = core.StatusInvalid
|
||||
+ challenge.Error = &core.ProblemDetails{
|
||||
+ Type: parseHTTPConnError(err),
|
||||
+ Detail: "Failed to resolve host for DVSNI challenge",
|
||||
+ }
|
||||
+ va.log.Debug(fmt.Sprintf("DVSNI [%s] DNS connection failure: %s", identifier, err))
|
||||
+ return challenge, err
|
||||
+ }
|
||||
+
|
||||
+ hostPort := ips[0].String()
|
||||
+ if port != "" {
|
||||
+ hostPort = net.JoinHostPort(hostPort, port)
|
||||
+ }
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", hostPort, &tls.Config{
|
||||
ServerName: nonceName,
|
||||
InsecureSkipVerify: true,
|
||||
|
|
@ -20,6 +20,9 @@ import (
|
|||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -80,14 +83,14 @@ func createValidation(token string, enableTLS bool) string {
|
|||
return obj.FullSerialize()
|
||||
}
|
||||
|
||||
func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableTLS bool) {
|
||||
func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
|
||||
m := http.NewServeMux()
|
||||
|
||||
defaultToken := token
|
||||
currentToken := defaultToken
|
||||
|
||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host != "localhost" && r.Host != "other.valid" && r.Host != "other.valid:8080" {
|
||||
if !strings.HasPrefix(r.Host, "localhost:") && r.Host != "other.valid" && r.Host != "other.valid:8080" {
|
||||
t.Errorf("Bad Host header: " + r.Host)
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, path404) {
|
||||
|
|
@ -133,21 +136,10 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
|
|||
}
|
||||
})
|
||||
|
||||
server := &http.Server{Addr: "localhost:5001", Handler: m}
|
||||
conn, err := net.Listen("tcp", server.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", server.Addr, err)
|
||||
}
|
||||
server := httptest.NewUnstartedServer(m)
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
var listener net.Listener
|
||||
if !enableTLS {
|
||||
listener = conn
|
||||
server.Start()
|
||||
} else {
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1337),
|
||||
|
|
@ -170,18 +162,17 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
|
|||
PrivateKey: &TheKey,
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
}
|
||||
|
||||
listener = tls.NewListener(conn, tlsConfig)
|
||||
server.StartTLS()
|
||||
}
|
||||
|
||||
waitChan <- true
|
||||
server.Serve(listener)
|
||||
return server
|
||||
}
|
||||
|
||||
func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool) {
|
||||
func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
|
||||
encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature)
|
||||
h := sha256.New()
|
||||
h.Write([]byte(encodedSig))
|
||||
|
|
@ -222,39 +213,21 @@ func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool)
|
|||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
|
||||
httpsServer := &http.Server{Addr: "localhost:5001"}
|
||||
conn, err := net.Listen("tcp", httpsServer.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
|
||||
}
|
||||
tlsListener := tls.NewListener(conn, tlsConfig)
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
waitChan <- true
|
||||
httpsServer.Serve(tlsListener)
|
||||
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
|
||||
hs.TLS = tlsConfig
|
||||
hs.StartTLS()
|
||||
return hs
|
||||
}
|
||||
|
||||
func brokenTLSSrv(t *testing.T, stopChan, waitChan chan bool) {
|
||||
httpsServer := &http.Server{Addr: "localhost:5001"}
|
||||
conn, err := net.Listen("tcp", httpsServer.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
|
||||
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")
|
||||
},
|
||||
}
|
||||
tlsListener := tls.NewListener(conn, &tls.Config{})
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
waitChan <- true
|
||||
httpsServer.Serve(tlsListener)
|
||||
server.StartTLS()
|
||||
return server
|
||||
}
|
||||
|
||||
func TestSimpleHttpTLS(t *testing.T) {
|
||||
|
|
@ -263,11 +236,12 @@ func TestSimpleHttpTLS(t *testing.T) {
|
|||
|
||||
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, ValidationRecord: []core.ValidationRecord{}}
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, true)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
hs := simpleSrv(t, expectedToken, true)
|
||||
defer hs.Close()
|
||||
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.simpleHTTPSPort = port
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
|
|
@ -279,22 +253,17 @@ func TestSimpleHttpTLS(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSimpleHttp(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
tls := false
|
||||
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
|
||||
|
||||
invalidChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
hs := simpleSrv(t, expectedToken, tls)
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.simpleHTTPPort = port
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
|
|
@ -304,7 +273,7 @@ func TestSimpleHttp(t *testing.T) {
|
|||
|
||||
log.Clear()
|
||||
chall.Token = path404
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
invalidChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Should have found a 404 for the challenge.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
|
|
@ -341,12 +310,10 @@ func TestSimpleHttp(t *testing.T) {
|
|||
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
va.TestMode = false
|
||||
invalidChall, err = va.validateSimpleHTTP(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Domain name is invalid.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
|
||||
va.TestMode = true
|
||||
|
||||
chall.Token = "wait-long"
|
||||
started := time.Now()
|
||||
|
|
@ -358,6 +325,13 @@ func TestSimpleHttp(t *testing.T) {
|
|||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Connection should've timed out")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
// Take down server and check that validation fails
|
||||
hs.Close()
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestSimpleHttpRedirectLookup(t *testing.T) {
|
||||
|
|
@ -367,11 +341,11 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
|
|||
tls := false
|
||||
chall := core.Challenge{Token: expectedToken, TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
hs := simpleSrv(t, expectedToken, tls)
|
||||
defer hs.Close()
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.simpleHTTPPort = port
|
||||
|
||||
log.Clear()
|
||||
chall.Token = pathMoved
|
||||
|
|
@ -425,11 +399,11 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
|
|||
tls := false
|
||||
chall := core.Challenge{Token: "looper", TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
hs := simpleSrv(t, expectedToken, tls)
|
||||
defer hs.Close()
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.simpleHTTPPort = port
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
|
|
@ -438,24 +412,33 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
|
|||
fmt.Println(finChall)
|
||||
}
|
||||
|
||||
func TestDvsni(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
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 TestDvsni(t *testing.T) {
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
|
||||
log.Clear()
|
||||
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
|
||||
hs := dvsniSrv(t, chall)
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
|
||||
waitChan := make(chan bool, 1)
|
||||
stopChan := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChan, waitChan)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.dvsniPort = port
|
||||
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
|
|
@ -464,20 +447,20 @@ func TestDvsni(t *testing.T) {
|
|||
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
|
||||
|
||||
log.Clear()
|
||||
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall, AccountKey)
|
||||
invalidChall, err := va.validateDvsni(core.AcmeIdentifier{
|
||||
Type: core.IdentifierType("ip"),
|
||||
Value: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)),
|
||||
}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
log.Clear()
|
||||
va.TestMode = false
|
||||
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Domain name is invalid.")
|
||||
test.AssertError(t, err, "Domain name was supposed to be invalid.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
|
||||
|
||||
va.TestMode = true
|
||||
|
||||
// Need to re-sign to get an unknown SNI (from the signature value)
|
||||
chall.Token = core.NewToken()
|
||||
validationPayload, _ := json.Marshal(map[string]interface{}{
|
||||
|
|
@ -498,18 +481,25 @@ func TestDvsni(t *testing.T) {
|
|||
test.AssertError(t, err, "Connection should've timed out")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
|
||||
|
||||
// Take down DVSNI validation server and check that validation fails.
|
||||
hs.Close()
|
||||
invalidChall, err = va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestTLSError(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChan := make(chan bool, 1)
|
||||
stopChan := make(chan bool, 1)
|
||||
go brokenTLSSrv(t, stopChan, waitChan)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
hs := brokenTLSSrv()
|
||||
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.dvsniPort = port
|
||||
|
||||
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
|
|
@ -518,7 +508,7 @@ func TestTLSError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateHTTP(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -528,17 +518,11 @@ func TestValidateHTTP(t *testing.T) {
|
|||
challHTTP.TLS = &tls
|
||||
challHTTP.ValidationRecord = []core.ValidationRecord{}
|
||||
|
||||
stopChanHTTP := make(chan bool, 1)
|
||||
waitChanHTTP := make(chan bool, 1)
|
||||
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
|
||||
|
||||
// Let them start
|
||||
<-waitChanHTTP
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanHTTP <- true
|
||||
}()
|
||||
hs := simpleSrv(t, challHTTP.Token, tls)
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.simpleHTTPPort = port
|
||||
defer hs.Close()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
|
|
@ -571,23 +555,18 @@ func createChallenge(challengeType string) core.Challenge {
|
|||
}
|
||||
|
||||
func TestValidateDvsni(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChanDvsni := make(chan bool, 1)
|
||||
stopChanDvsni := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
|
||||
hs := dvsniSrv(t, chall)
|
||||
defer hs.Close()
|
||||
|
||||
// Let them start
|
||||
<-waitChanDvsni
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanDvsni <- true
|
||||
}()
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va.dvsniPort = port
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
|
|
@ -601,23 +580,12 @@ func TestValidateDvsni(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateDvsniNotSane(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChanDvsni := make(chan bool, 1)
|
||||
stopChanDvsni := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
|
||||
|
||||
// Let them start
|
||||
<-waitChanDvsni
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanDvsni <- true
|
||||
}()
|
||||
|
||||
chall.Token = "not sane"
|
||||
|
||||
|
|
@ -633,7 +601,7 @@ func TestValidateDvsniNotSane(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdateValidations(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -643,18 +611,6 @@ func TestUpdateValidations(t *testing.T) {
|
|||
challHTTP.TLS = &tls
|
||||
challHTTP.ValidationRecord = []core.ValidationRecord{}
|
||||
|
||||
stopChanHTTP := make(chan bool, 1)
|
||||
waitChanHTTP := make(chan bool, 1)
|
||||
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
|
||||
|
||||
// Let them start
|
||||
<-waitChanHTTP
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanHTTP <- true
|
||||
}()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
|
|
@ -700,7 +656,7 @@ func TestCAAChecking(t *testing.T) {
|
|||
// CNAME to critical
|
||||
}
|
||||
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
va.IssuerDomain = "letsencrypt.org"
|
||||
for _, caaTest := range tests {
|
||||
|
|
@ -732,7 +688,7 @@ func TestCAAChecking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDNSValidationFailure(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -768,7 +724,7 @@ func TestDNSValidationInvalid(t *testing.T) {
|
|||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -781,7 +737,7 @@ func TestDNSValidationInvalid(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDNSValidationNotSane(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -811,7 +767,7 @@ func TestDNSValidationNotSane(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDNSValidationServFail(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
@ -836,7 +792,7 @@ func TestDNSValidationServFail(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDNSValidationNoServer(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = core.NewDNSResolverImpl(time.Second*5, []string{})
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
|
|
|||
|
|
@ -0,0 +1,834 @@
|
|||
// Copyright 2014 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package va
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"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 log = mocks.UseMockLog()
|
||||
|
||||
const expectedToken = "THETOKEN"
|
||||
const pathWrongToken = "wrongtoken"
|
||||
const path404 = "404"
|
||||
const pathFound = "302"
|
||||
const pathMoved = "301"
|
||||
|
||||
func createValidation(token string, enableTLS bool) string {
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "simpleHttp",
|
||||
"token": token,
|
||||
"tls": enableTLS,
|
||||
})
|
||||
signer, _ := jose.NewSigner(jose.RS256, &TheKey)
|
||||
obj, _ := signer.Sign(payload, "")
|
||||
return obj.FullSerialize()
|
||||
}
|
||||
|
||||
func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableTLS bool) {
|
||||
m := http.NewServeMux()
|
||||
|
||||
defaultToken := token
|
||||
currentToken := defaultToken
|
||||
|
||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, path404) {
|
||||
t.Logf("SIMPLESRV: Got a 404 req\n")
|
||||
http.NotFound(w, r)
|
||||
} else if strings.HasSuffix(r.URL.Path, pathMoved) {
|
||||
t.Logf("SIMPLESRV: Got a 301 redirect req\n")
|
||||
if currentToken == defaultToken {
|
||||
currentToken = pathMoved
|
||||
}
|
||||
http.Redirect(w, r, "valid", 301)
|
||||
} else if strings.HasSuffix(r.URL.Path, pathFound) {
|
||||
t.Logf("SIMPLESRV: Got a 302 redirect req\n")
|
||||
if currentToken == defaultToken {
|
||||
currentToken = pathFound
|
||||
}
|
||||
http.Redirect(w, r, pathMoved, 302)
|
||||
} else if strings.HasSuffix(r.URL.Path, "wait") {
|
||||
t.Logf("SIMPLESRV: Got a wait req\n")
|
||||
time.Sleep(time.Second * 3)
|
||||
} else if strings.HasSuffix(r.URL.Path, "wait-long") {
|
||||
t.Logf("SIMPLESRV: Got a wait-long req\n")
|
||||
time.Sleep(time.Second * 10)
|
||||
} else {
|
||||
t.Logf("SIMPLESRV: Got a valid req\n")
|
||||
fmt.Fprintf(w, "%s", createValidation(currentToken, enableTLS))
|
||||
currentToken = defaultToken
|
||||
}
|
||||
})
|
||||
|
||||
server := &http.Server{Addr: "localhost:5001", Handler: m}
|
||||
conn, err := net.Listen("tcp", server.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", server.Addr, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
var listener net.Listener
|
||||
if !enableTLS {
|
||||
listener = conn
|
||||
} else {
|
||||
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{"example.com"},
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
|
||||
listener = tls.NewListener(conn, tlsConfig)
|
||||
}
|
||||
|
||||
waitChan <- true
|
||||
server.Serve(listener)
|
||||
}
|
||||
|
||||
func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool) {
|
||||
encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature)
|
||||
h := sha256.New()
|
||||
h.Write([]byte(encodedSig))
|
||||
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"},
|
||||
}
|
||||
|
||||
httpsServer := &http.Server{Addr: "localhost:5001"}
|
||||
conn, err := net.Listen("tcp", httpsServer.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
|
||||
}
|
||||
tlsListener := tls.NewListener(conn, tlsConfig)
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
waitChan <- true
|
||||
httpsServer.Serve(tlsListener)
|
||||
}
|
||||
|
||||
func brokenTLSSrv(t *testing.T, stopChan, waitChan chan bool) {
|
||||
httpsServer := &http.Server{Addr: "localhost:5001"}
|
||||
conn, err := net.Listen("tcp", httpsServer.Addr)
|
||||
if err != nil {
|
||||
waitChan <- true
|
||||
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
|
||||
}
|
||||
tlsListener := tls.NewListener(conn, &tls.Config{})
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
waitChan <- true
|
||||
httpsServer.Serve(tlsListener)
|
||||
}
|
||||
|
||||
func TestSimpleHttpTLS(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken}
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, true)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "Error validating simpleHttp")
|
||||
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate SimpleHTTPS for `)
|
||||
test.AssertEquals(t, len(logs), 1)
|
||||
test.AssertEquals(t, logs[0].Priority, syslog.LOG_NOTICE)
|
||||
}
|
||||
|
||||
func TestSimpleHttp(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
tls := false
|
||||
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, TLS: &tls}
|
||||
|
||||
invalidChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
stopChan := make(chan bool, 1)
|
||||
waitChan := make(chan bool, 1)
|
||||
go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "Error validating simpleHttp")
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Token = path404
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Should have found a 404 for the challenge.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Token = pathWrongToken
|
||||
// The "wrong token" will actually be the expectedToken. It's wrong
|
||||
// because it doesn't match pathWrongToken.
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Should have found the wrong token value.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Token = pathMoved
|
||||
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "Failed to follow 301 redirect")
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Token = pathFound
|
||||
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "Failed to follow 302 redirect")
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
|
||||
|
||||
ipIdentifier := core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}
|
||||
invalidChall, err = va.validateSimpleHTTP(ipIdentifier, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
va.TestMode = false
|
||||
invalidChall, err = va.validateSimpleHTTP(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Domain name is invalid.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
|
||||
va.TestMode = true
|
||||
|
||||
chall.Token = "wait-long"
|
||||
started := time.Now()
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
|
||||
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")
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Connection should've timed out")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestDvsni(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
|
||||
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
waitChan := make(chan bool, 1)
|
||||
stopChan := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChan, waitChan)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
finChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "")
|
||||
|
||||
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
va.TestMode = false
|
||||
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Domain name is invalid.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
|
||||
|
||||
va.TestMode = true
|
||||
|
||||
// Need to re-sign to get an unknown SNI (from the signature value)
|
||||
chall.Token = core.NewToken()
|
||||
validationPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"type": chall.Type,
|
||||
"token": chall.Token,
|
||||
})
|
||||
signer, _ := jose.NewSigner(jose.RS256, &TheKey)
|
||||
chall.Validation, _ = signer.Sign(validationPayload, "")
|
||||
|
||||
started := time.Now()
|
||||
invalidChall, err = va.validateDvsni(ident, chall, AccountKey)
|
||||
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")
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Connection should've timed out")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestTLSError(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChan := make(chan bool, 1)
|
||||
stopChan := make(chan bool, 1)
|
||||
go brokenTLSSrv(t, stopChan, waitChan)
|
||||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "What cert was used?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem)
|
||||
}
|
||||
|
||||
func TestValidateHTTP(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
tls := false
|
||||
challHTTP := core.SimpleHTTPChallenge()
|
||||
challHTTP.TLS = &tls
|
||||
|
||||
stopChanHTTP := make(chan bool, 1)
|
||||
waitChanHTTP := make(chan bool, 1)
|
||||
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
|
||||
|
||||
// Let them start
|
||||
<-waitChanHTTP
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanHTTP <- true
|
||||
}()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{challHTTP},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status)
|
||||
}
|
||||
|
||||
// challengeType == "dvsni" or "dns", since they're the same
|
||||
func createChallenge(challengeType string) core.Challenge {
|
||||
chall := core.Challenge{
|
||||
Type: challengeType,
|
||||
Status: core.StatusPending,
|
||||
Token: core.NewToken(),
|
||||
}
|
||||
|
||||
validationPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"type": chall.Type,
|
||||
"token": chall.Token,
|
||||
})
|
||||
|
||||
signer, _ := jose.NewSigner(jose.RS256, &TheKey)
|
||||
chall.Validation, _ = signer.Sign(validationPayload, "")
|
||||
return chall
|
||||
}
|
||||
|
||||
func TestValidateDvsni(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChanDvsni := make(chan bool, 1)
|
||||
stopChanDvsni := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
|
||||
|
||||
// Let them start
|
||||
<-waitChanDvsni
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanDvsni <- true
|
||||
}()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{chall},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status)
|
||||
}
|
||||
|
||||
func TestValidateDvsniNotSane(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chall := createChallenge(core.ChallengeTypeDVSNI)
|
||||
waitChanDvsni := make(chan bool, 1)
|
||||
stopChanDvsni := make(chan bool, 1)
|
||||
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
|
||||
|
||||
// Let them start
|
||||
<-waitChanDvsni
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanDvsni <- true
|
||||
}()
|
||||
|
||||
chall.Token = "not sane"
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{chall},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertEquals(t, core.StatusInvalid, mockRA.lastAuthz.Challenges[0].Status)
|
||||
}
|
||||
|
||||
func TestUpdateValidations(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
tls := false
|
||||
challHTTP := core.SimpleHTTPChallenge()
|
||||
challHTTP.TLS = &tls
|
||||
|
||||
stopChanHTTP := make(chan bool, 1)
|
||||
waitChanHTTP := make(chan bool, 1)
|
||||
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
|
||||
|
||||
// Let them start
|
||||
<-waitChanHTTP
|
||||
|
||||
// shutdown cleanly
|
||||
defer func() {
|
||||
stopChanHTTP <- true
|
||||
}()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{challHTTP},
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
va.UpdateValidations(authz, 0, AccountKey)
|
||||
took := time.Since(started)
|
||||
|
||||
// Check that the call to va.UpdateValidations didn't block for 3 seconds
|
||||
test.Assert(t, (took < (time.Second * 3)), "UpdateValidations blocked")
|
||||
}
|
||||
|
||||
func TestCAAChecking(t *testing.T) {
|
||||
type CAATest struct {
|
||||
Domain string
|
||||
Present bool
|
||||
Valid bool
|
||||
}
|
||||
tests := []CAATest{
|
||||
// Reserved
|
||||
CAATest{"reserved.com", true, false},
|
||||
// Critical
|
||||
CAATest{"critical.com", true, false},
|
||||
CAATest{"nx.critical.com", true, false},
|
||||
CAATest{"cname-critical.com", true, false},
|
||||
CAATest{"nx.cname-critical.com", true, false},
|
||||
// Good (absent)
|
||||
CAATest{"absent.com", false, true},
|
||||
CAATest{"cname-absent.com", false, true},
|
||||
CAATest{"nx.cname-absent.com", false, true},
|
||||
CAATest{"cname-nx.com", false, true},
|
||||
CAATest{"example.co.uk", false, true},
|
||||
// Good (present)
|
||||
CAATest{"present.com", true, true},
|
||||
CAATest{"cname-present.com", true, true},
|
||||
CAATest{"cname2-present.com", true, true},
|
||||
CAATest{"nx.cname2-present.com", true, true},
|
||||
CAATest{"dname-present.com", true, true},
|
||||
CAATest{"dname2cname.com", true, true},
|
||||
// CNAME to critical
|
||||
}
|
||||
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
va.IssuerDomain = "letsencrypt.org"
|
||||
for _, caaTest := range tests {
|
||||
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
|
||||
test.AssertNotError(t, err, caaTest.Domain)
|
||||
fmt.Println(caaTest.Domain, caaTest.Present == present, caaTest.Valid == valid)
|
||||
test.AssertEquals(t, caaTest.Present, present)
|
||||
test.AssertEquals(t, caaTest.Valid, valid)
|
||||
}
|
||||
|
||||
present, valid, err := va.CheckCAARecords(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")
|
||||
|
||||
for _, name := range []string{
|
||||
"www.caa-loop.com",
|
||||
"a.cname-loop.com",
|
||||
"a.dname-loop.com",
|
||||
"cname-servfail.com",
|
||||
"cname2servfail.com",
|
||||
"dname-servfail.com",
|
||||
"cname-and-dname.com",
|
||||
"servfail.com",
|
||||
} {
|
||||
_, _, err = va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: name})
|
||||
test.AssertError(t, err, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSValidationFailure(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chalDNS := createChallenge(core.ChallengeTypeDNS)
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
t.Logf("Resulting Authz: %+v", authz)
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.UnauthorizedProblem)
|
||||
}
|
||||
|
||||
func TestDNSValidationInvalid(t *testing.T) {
|
||||
var notDNS = core.AcmeIdentifier{
|
||||
Type: core.IdentifierType("iris"),
|
||||
Value: "790DB180-A274-47A4-855F-31C428CB1072",
|
||||
}
|
||||
|
||||
chalDNS := core.DNSChallenge()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: notDNS,
|
||||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.MalformedProblem)
|
||||
}
|
||||
|
||||
func TestDNSValidationNotSane(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chal0 := core.DNSChallenge()
|
||||
chal0.Token = ""
|
||||
|
||||
chal1 := core.DNSChallenge()
|
||||
chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
|
||||
|
||||
chal2 := core.DNSChallenge()
|
||||
chal2.TLS = new(bool)
|
||||
*chal2.TLS = true
|
||||
|
||||
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++ {
|
||||
va.validate(authz, i, AccountKey)
|
||||
test.AssertEquals(t, authz.Challenges[i].Status, core.StatusInvalid)
|
||||
test.AssertEquals(t, authz.Challenges[i].Error.Type, core.MalformedProblem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSValidationServFail(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chalDNS := createChallenge(core.ChallengeTypeDNS)
|
||||
|
||||
badIdent := core.AcmeIdentifier{
|
||||
Type: core.IdentifierDNS,
|
||||
Value: "servfail.com",
|
||||
}
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: badIdent,
|
||||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestDNSValidationNoServer(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = core.NewDNSResolverImpl(time.Second*5, []string{})
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
chalDNS := createChallenge(core.ChallengeTypeDNS)
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: ident,
|
||||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
va.validate(authz, 0, AccountKey)
|
||||
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
// TestDNSValidationLive is an integration test, depending on
|
||||
// the existance of some Internet resources. Because of that,
|
||||
// it asserts nothing; it is intended for coverage.
|
||||
func TestDNSValidationLive(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(false)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
||||
goodChalDNS := core.DNSChallenge()
|
||||
// This token is set at _acme-challenge.good.bin.coffee
|
||||
goodChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"
|
||||
|
||||
var goodIdent = core.AcmeIdentifier{
|
||||
Type: core.IdentifierDNS,
|
||||
Value: "good.bin.coffee",
|
||||
}
|
||||
|
||||
var badIdent = core.AcmeIdentifier{
|
||||
Type: core.IdentifierType("dns"),
|
||||
Value: "bad.bin.coffee",
|
||||
}
|
||||
|
||||
var authzGood = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: goodIdent,
|
||||
Challenges: []core.Challenge{goodChalDNS},
|
||||
}
|
||||
|
||||
va.validate(authzGood, 0, AccountKey)
|
||||
|
||||
if authzGood.Challenges[0].Status != core.StatusValid {
|
||||
t.Logf("TestDNSValidationLive on Good did not succeed.")
|
||||
}
|
||||
|
||||
badChalDNS := core.DNSChallenge()
|
||||
// This token is NOT set at _acme-challenge.bad.bin.coffee
|
||||
badChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"
|
||||
|
||||
var authzBad = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: badIdent,
|
||||
Challenges: []core.Challenge{badChalDNS},
|
||||
}
|
||||
|
||||
va.validate(authzBad, 0, AccountKey)
|
||||
if authzBad.Challenges[0].Status != core.StatusInvalid {
|
||||
t.Logf("TestDNSValidationLive on Bad did succeed inappropriately.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MockRegistrationAuthority struct {
|
||||
lastAuthz *core.Authorization
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) NewRegistration(reg core.Registration) (core.Registration, error) {
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) NewAuthorization(authz core.Authorization, regID int64) (core.Authorization, error) {
|
||||
return authz, nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) NewCertificate(req core.CertificateRequest, regID int64) (core.Certificate, error) {
|
||||
return core.Certificate{}, nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) UpdateRegistration(reg core.Registration, updated core.Registration) (core.Registration, error) {
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) UpdateAuthorization(authz core.Authorization, foo int, challenge core.Challenge) (core.Authorization, error) {
|
||||
return authz, nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) RevokeCertificate(cert x509.Certificate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ra *MockRegistrationAuthority) OnValidationUpdate(authz core.Authorization) error {
|
||||
ra.lastAuthz = &authz
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
--- va/validation-authority_test.go
|
||||
+++ va/validation-authority_test.go
|
||||
@@ -121,18 +112,15 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
|
||||
PrivateKey: &TheKey,
|
||||
}
|
||||
|
||||
- tlsConfig := &tls.Config{
|
||||
+ server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
}
|
||||
-
|
||||
- listener = tls.NewListener(conn, tlsConfig)
|
||||
+ server.StartTLS()
|
||||
}
|
||||
-
|
||||
- waitChan <- true
|
||||
- server.Serve(listener)
|
||||
+ return server
|
||||
}
|
||||
|
||||
-func dvsniSrv(t *testing.T, R, S []byte, stopChan, waitChan chan bool) {
|
||||
+func dvsniSrv(t *testing.T, R, S []byte) *httptest.Server {
|
||||
RS := append(R, S...)
|
||||
z := sha256.Sum256(RS)
|
||||
zName := fmt.Sprintf("%064x.acme.invalid", z)
|
||||
@@ -200,14 +177,16 @@ func TestSimpleHttpTLS(t *testing.T) {
|
||||
|
||||
chall := core.Challenge{Path: "test", Token: expectedToken}
|
||||
|
||||
- stopChan := make(chan bool, 1)
|
||||
- waitChan := make(chan bool, 1)
|
||||
- go simpleSrv(t, expectedToken, stopChan, waitChan, true)
|
||||
- defer func() { stopChan <- true }()
|
||||
- <-waitChan
|
||||
+ hs := simpleSrv(t, expectedToken, true)
|
||||
+ defer hs.Close()
|
||||
+ u, err := url.Parse(hs.URL)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("unable to parse httptest.Server URL %#v: %#v", hs.URL, err)
|
||||
+ }
|
||||
+ idt := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: u.Host}
|
||||
|
||||
log.Clear()
|
||||
- finChall, err := va.validateSimpleHTTP(ident, chall)
|
||||
+ finChall, err := va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate SimpleHTTPS for `)
|
||||
@@ -227,28 +206,30 @@ func TestSimpleHttp(t *testing.T) {
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
- stopChan := make(chan bool, 1)
|
||||
- waitChan := make(chan bool, 1)
|
||||
- go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
|
||||
- defer func() { stopChan <- true }()
|
||||
- <-waitChan
|
||||
+ hs := simpleSrv(t, expectedToken, tls)
|
||||
+ defer hs.Close()
|
||||
+ u, err := url.Parse(hs.URL)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("unable to parse httptest.Server URL %#v: %#v", hs.URL, err)
|
||||
+ }
|
||||
+ idt := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: u.Host}
|
||||
|
||||
log.Clear()
|
||||
- finChall, err := va.validateSimpleHTTP(ident, chall)
|
||||
+ finChall, err := va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Path = pathMoved
|
||||
- finChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ finChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Path = pathFound
|
||||
- finChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ finChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
|
||||
@@ -256,20 +237,20 @@ func TestSimpleHttp(t *testing.T) {
|
||||
|
||||
log.Clear()
|
||||
chall.Path = path404
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Should have found a 404 for the challenge.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
chall.Path = pathWrongToken
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "The path should have given us the wrong token.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
|
||||
chall.Path = ""
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Empty paths shouldn't work either.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
@@ -289,20 +270,20 @@ func TestSimpleHttp(t *testing.T) {
|
||||
va.TestMode = true
|
||||
|
||||
chall.Path = pathUnsafe
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Path doesn't consist of URL-safe characters.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
chall.Path = pathUnsafe302
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Redirect should have failed.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
chall.Path = "wait-long"
|
||||
started := time.Now()
|
||||
- invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
+ invalidChall, err = va.validateSimpleHTTP(idt, 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")
|
||||
@@ -314,28 +295,36 @@ func TestSimpleHttp(t *testing.T) {
|
||||
|
||||
func TestDvsni(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
+ // FIXME
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
-
|
||||
a := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
|
||||
ba := core.B64enc(a)
|
||||
chall := core.Challenge{R: ba, S: ba}
|
||||
|
||||
+ // FIXME tear down server then test this with real idt
|
||||
invalidChall, err := va.validateDvsni(ident, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
|
||||
|
||||
- waitChan := make(chan bool, 1)
|
||||
- stopChan := make(chan bool, 1)
|
||||
- go dvsniSrv(t, a, a, stopChan, waitChan)
|
||||
- defer func() { stopChan <- true }()
|
||||
- <-waitChan
|
||||
+ hs := dvsniSrv(t, a, a)
|
||||
+ defer hs.Close()
|
||||
+
|
||||
+ u, err := url.Parse(hs.URL)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("unable to parse httptest.Server URL %#v: %#v", hs.URL, err)
|
||||
+ }
|
||||
+ _, port, err := net.SplitHostPort(u.Host)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("unable to SplitHostPort the httptestServer URL %#v: %#v", hs.URL, err)
|
||||
+ }
|
||||
+ idt := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: u.Host}
|
||||
|
||||
- finChall, err := va.validateDvsni(ident, chall)
|
||||
+ finChall, err := va.validateDvsni(idt, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, "")
|
||||
|
||||
- invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall)
|
||||
+ invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: net.JoinHostPort("127.0.0.1", port)}, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
@@ -348,22 +337,22 @@ func TestDvsni(t *testing.T) {
|
||||
|
||||
va.TestMode = true
|
||||
chall.R = ba[5:]
|
||||
- invalidChall, err = va.validateDvsni(ident, chall)
|
||||
+ invalidChall, err = va.validateDvsni(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "R Should be illegal Base64")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
chall.R = ba
|
||||
chall.S = "!@#"
|
||||
- invalidChall, err = va.validateDvsni(ident, chall)
|
||||
+ invalidChall, err = va.validateDvsni(idt, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "S Should be illegal Base64")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
|
||||
|
||||
chall.S = ba
|
||||
- chall.Nonce = "wait-long"
|
||||
+ chall.Nonce = "wait-long" // FIXME make a constant to be shared
|
||||
started := time.Now()
|
||||
- invalidChall, err = va.validateDvsni(ident, chall)
|
||||
+ invalidChall, err = va.validateDvsni(idt, 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")
|
||||
@@ -427,19 +407,11 @@ func TestValidateDvsni(t *testing.T) {
|
||||
challDvsni := core.DvsniChallenge()
|
||||
challDvsni.S = challDvsni.R
|
||||
|
||||
- waitChanDvsni := make(chan bool, 1)
|
||||
- stopChanDvsni := make(chan bool, 1)
|
||||
ar, _ := core.B64dec(challDvsni.R)
|
||||
as, _ := core.B64dec(challDvsni.S)
|
||||
- go dvsniSrv(t, ar, as, stopChanDvsni, waitChanDvsni)
|
||||
+ hs := dvsniSrv(t, ar, as)
|
||||
|
||||
- // Let them start
|
||||
- <-waitChanDvsni
|
||||
-
|
||||
- // shutdown cleanly
|
||||
- defer func() {
|
||||
- stopChanDvsni <- true
|
||||
- }()
|
||||
+ defer hs.Close()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
@@ -461,19 +433,12 @@ func TestValidateDvsniNotSane(t *testing.T) {
|
||||
challDvsni := core.DvsniChallenge()
|
||||
challDvsni.R = "boulder" // Not a sane thing to do.
|
||||
|
||||
- waitChanDvsni := make(chan bool, 1)
|
||||
- stopChanDvsni := make(chan bool, 1)
|
||||
ar, _ := core.B64dec(challDvsni.R)
|
||||
as, _ := core.B64dec(challDvsni.S)
|
||||
- go dvsniSrv(t, ar, as, stopChanDvsni, waitChanDvsni)
|
||||
-
|
||||
- // Let them start
|
||||
- <-waitChanDvsni
|
||||
+ hs := dvsniSrv(t, ar, as)
|
||||
|
||||
// shutdown cleanly
|
||||
- defer func() {
|
||||
- stopChanDvsni <- true
|
||||
- }()
|
||||
+ defer hs.Close()
|
||||
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
Loading…
Reference in New Issue