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:
Jacob Hoffman-Andrews 2015-08-20 19:43:07 -07:00
parent 803193a19e
commit efa94628c7
6 changed files with 1890 additions and 190 deletions

View File

@ -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{

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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
}

View File

@ -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(),