diff --git a/va/validation-authority.go b/va/validation-authority.go index ea2cf456a..d08e0b91b 100644 --- a/va/validation-authority.go +++ b/va/validation-authority.go @@ -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{ diff --git a/va/validation-authority.go.orig b/va/validation-authority.go.orig new file mode 100644 index 000000000..3130e6f25 --- /dev/null +++ b/va/validation-authority.go.orig @@ -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 +} diff --git a/va/validation-authority.go.rej b/va/validation-authority.go.rej new file mode 100644 index 000000000..554e14ea7 --- /dev/null +++ b/va/validation-authority.go.rej @@ -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, diff --git a/va/validation-authority_test.go b/va/validation-authority_test.go index 2c725eab1..e0caf853d 100644 --- a/va/validation-authority_test.go +++ b/va/validation-authority_test.go @@ -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 diff --git a/va/validation-authority_test.go.orig b/va/validation-authority_test.go.orig new file mode 100644 index 000000000..01b57e2b3 --- /dev/null +++ b/va/validation-authority_test.go.orig @@ -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 +} diff --git a/va/validation-authority_test.go.rej b/va/validation-authority_test.go.rej new file mode 100644 index 000000000..b5f95c896 --- /dev/null +++ b/va/validation-authority_test.go.rej @@ -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(),