Merge pull request #1277 from letsencrypt/dns-integration

Enable DNS challenge integration tests
This commit is contained in:
Jacob Hoffman-Andrews 2015-12-16 18:23:56 -08:00
commit f3d368eaff
4 changed files with 156 additions and 45 deletions

View File

@ -326,7 +326,7 @@ type Challenge struct {
// RecordsSane checks the sanity of a ValidationRecord object before sending it
// back to the RA to be stored.
func (ch Challenge) RecordsSane() bool {
if ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0 {
if ch.Type != ChallengeTypeDNS01 && (ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0) {
return false
}
@ -350,7 +350,7 @@ func (ch Challenge) RecordsSane() bool {
return false
}
case ChallengeTypeDNS01:
// Nothing for now
return true
default: // Unsupported challenge type
return false
}

View File

@ -6,15 +6,60 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
type testSrv struct {
mu *sync.RWMutex
txtRecords map[string]string
}
type setRequest struct {
Host string `json:"host"`
Value string `json:"value"`
}
func (ts *testSrv) setTXT(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/set-txt" {
http.NotFound(w, r)
return
} else if r.Method != "POST" {
w.WriteHeader(405)
return
}
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var sr setRequest
err = json.Unmarshal(msg, &sr)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if sr.Host == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
ts.mu.Lock()
defer ts.mu.Unlock()
ts.txtRecords[strings.ToLower(sr.Host)] = sr.Value
fmt.Printf("dns-srv: added TXT record for %s containing \"%s\"\n", sr.Host, sr.Value)
w.WriteHeader(http.StatusOK)
}
func (ts *testSrv) dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
m.Compress = false
@ -52,6 +97,22 @@ func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
record.Mx = "mail." + q.Name
record.Preference = 10
m.Answer = append(m.Answer, record)
case dns.TypeTXT:
ts.mu.RLock()
value, present := ts.txtRecords[q.Name]
ts.mu.RUnlock()
if !present {
continue
}
record := new(dns.TXT)
record.Hdr = dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 0,
}
record.Txt = []string{value}
m.Answer = append(m.Answer, record)
case dns.TypeCAA:
if q.Name == "bad-caa-reserved.com." || q.Name == "good-caa-reserved.com." {
@ -77,16 +138,27 @@ func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
return
}
func serveTestResolver() {
dns.HandleFunc(".", dnsHandler)
server := &dns.Server{
func (ts *testSrv) serveTestResolver() {
dns.HandleFunc(".", ts.dnsHandler)
dnsServer := &dns.Server{
Addr: "127.0.0.1:8053",
Net: "tcp",
ReadTimeout: time.Millisecond,
WriteTimeout: time.Millisecond,
}
go func() {
err := server.ListenAndServe()
err := dnsServer.ListenAndServe()
if err != nil {
fmt.Println(err)
return
}
}()
webServer := &http.Server{
Addr: "localhost:8055",
Handler: http.HandlerFunc(ts.setTXT),
}
go func() {
err := webServer.ListenAndServe()
if err != nil {
fmt.Println(err)
return
@ -96,7 +168,8 @@ func serveTestResolver() {
func main() {
fmt.Println("dns-srv: Starting test DNS server")
serveTestResolver()
ts := testSrv{mu: new(sync.RWMutex), txtRecords: make(map[string]string)}
ts.serveTestResolver()
forever := make(chan bool, 1)
<-forever
}

View File

@ -16,6 +16,8 @@ import urllib2
import startservers
ISSUANCE_FAILED = 1
REVOCATION_FAILED = 2
class ExitStatus:
OK, PythonFailure, NodeFailure, Error, OCSPFailure, CTFailure, IncorrectCommandLineArgs = range(7)
@ -151,23 +153,22 @@ def verify_ct_submission(expectedSubmissions, url):
if int(submissionStr) != expectedSubmissions:
print "Expected %d submissions, found %d" % (expectedSubmissions, int(submissionStr))
die(ExitStatus.CTFailure)
return 0
def run_node_test():
def run_node_test(domain, chall_type, expected_ct_submissions):
cert_file = os.path.join(tempdir, "cert.der")
cert_file_pem = os.path.join(tempdir, "cert.pem")
key_file = os.path.join(tempdir, "key.pem")
# Pick a random hostname so we don't run into certificate rate limiting.
domain = subprocess.check_output("openssl rand -hex 6", shell=True).strip()
# Issue the certificate and transform it from DER-encoded to PEM-encoded.
if subprocess.Popen('''
node test.js --email foo@letsencrypt.org --agree true \
--domains www.%s-TEST.com --new-reg http://localhost:4000/acme/new-reg \
--certKey %s --cert %s && \
--domains %s --new-reg http://localhost:4000/acme/new-reg \
--certKey %s --cert %s --challType %s && \
openssl x509 -in %s -out %s -inform der -outform pem
''' % (domain, key_file, cert_file, cert_file, cert_file_pem),
''' % (domain, key_file, cert_file, chall_type, cert_file, cert_file_pem),
shell=True).wait() != 0:
print("\nIssuing failed")
die(ExitStatus.NodeFailure)
return ISSUANCE_FAILED
ee_ocsp_url = "http://localhost:4002"
issuer_ocsp_url = "http://localhost:4003"
@ -180,38 +181,17 @@ def run_node_test():
# pre-signed, long-lived response for the CA cert, works.
wait_for_ocsp_good("../test-ca.pem", "../test-root.pem", issuer_ocsp_url)
verify_ct_submission(1, "http://localhost:4500/submissions")
verify_ct_submission(expected_ct_submissions, "http://localhost:4500/submissions")
if subprocess.Popen('''
node revoke.js %s %s http://localhost:4000/acme/revoke-cert
''' % (cert_file, key_file), shell=True).wait() != 0:
print("\nRevoking failed")
die(ExitStatus.NodeFailure)
return REVOCATION_FAILED
wait_for_ocsp_revoked(cert_file_pem, "../test-ca.pem", ee_ocsp_url)
return 0
def run_caa_node_test():
cert_file = os.path.join(tempdir, "cert.der")
key_file = os.path.join(tempdir, "key.pem")
def runNode(domain):
return subprocess.Popen('''
node test.js --email foo@letsencrypt.org --agree true \
--domains %s --new-reg http://localhost:4000/acme/new-reg \
--certKey %s --cert %s
''' % (domain, key_file, cert_file),
shell=True).wait()
if runNode("bad-caa-reserved.com") == 0:
print("\nIssused certificate for domain with bad CAA records")
die(ExitStatus.NodeFailure)
if runNode("good-caa-reserved.com") != 0:
print("\nDidn't issue certificate for domain with good CAA records")
die(ExitStatus.NodeFailure)
def run_client_tests():
root = os.environ.get("LETSENCRYPT_PATH")
assert root is not None, (
@ -263,8 +243,27 @@ def main():
if subprocess.Popen('npm install', shell=True).wait() != 0:
print("\n Installing NPM modules failed")
die(ExitStatus.Error)
run_node_test()
run_caa_node_test()
# Pick a random hostname so we don't run into certificate rate limiting.
domain = "www." + subprocess.check_output("openssl rand -hex 6", shell=True).strip() + "-TEST.com"
challenge_types = ["http-01", "dns-01"]
expected_ct_submissions = 1
resp = urllib2.urlopen("http://localhost:4500/submissions")
submissionStr = resp.read()
if int(submissionStr) > 0:
expected_ct_submissions = int(submissionStr)+1
for chall_type in challenge_types:
if run_node_test(domain, chall_type, expected_ct_submissions) != 0:
die(ExitStatus.NodeFailure)
expected_ct_submissions += 1
if run_node_test("good-caa-reserved.com", challenge_types[0], expected_ct_submissions) != 0:
print("\nDidn't issue certificate for domain with good CAA records")
die(ExitStatus.NodeFailure)
if run_node_test("bad-caa-reserved.com", challenge_types[0], expected_ct_submissions) != ISSUANCE_FAILED:
print("\nIssused certificate for domain with bad CAA records")
die(ExitStatus.NodeFailure)
# Simulate a disconnection from RabbitMQ to make sure reconnects work.
startservers.bounce_forward()

View File

@ -98,6 +98,7 @@ var cliOptions = cli.parse({
email: ["email", "Email address", "string", null],
agreeTerms: ["agree", "Agree to terms of service", "boolean", null],
domains: ["domains", "Domain name(s) for which to request a certificate (comma-separated)", "string", null],
challType: ["challType", "Name of challenge type to use for validations", "string", "http-01"],
});
var state = {
@ -347,19 +348,57 @@ function getReadyToValidate(err, resp, body) {
var authz = JSON.parse(body);
var httpChallenges = authz.challenges.filter(function(x) { return x.type == "http-01"; });
if (httpChallenges.length == 0) {
var challenges = authz.challenges.filter(function(x) { return x.type == cliOptions.challType; });
if (challenges.length == 0) {
console.log("The server didn't offer any challenges we can handle.");
process.exit(1);
}
var challenge = httpChallenges[0];
state.responseURL = challenges[0]["uri"];
var validator;
if (cliOptions.challType == "http-01") {
validator = validateHttp01;
} else if (cliOptions.challType == "dns-01") {
validator = validateDns01;
}
validator(challenges[0]);
}
function validateDns01(challenge) {
// Construct a key authorization for this token and key, and the
// correct record name to store it
var thumbprint = cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
var keyAuthorization = challenge.token + "." + thumbprint;
var recordName = "_acme-challenge." + state.domain + ".";
function txtCallback(err, resp, body) {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Updating dns-test-srv failed with code " + resp.statusCode);
process.exit(1);
}
post(state.responseURL, {
resource: "challenge",
keyAuthorization: keyAuthorization,
}, ensureValidation);
}
request.post({
uri: "http://localhost:8055/set-txt",
method: "POST",
json: {
"host": recordName,
"value": cryptoUtil.sha256(new Buffer(keyAuthorization))
}
}, txtCallback);
}
function validateHttp01(challenge) {
// Construct a key authorization for this token and key
var thumbprint = cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
var keyAuthorization = challenge.token + "." + thumbprint;
var challengePath = ".well-known/acme-challenge/" + challenge.token;
state.responseURL = challenge["uri"];
state.path = challengePath;
// For local, test-mode validation
@ -401,7 +440,7 @@ function ensureValidation(err, resp, body) {
var authz = JSON.parse(body);
if (authz.status != "pending") {
if (authz.status != "pending" && state.httpServer != null) {
state.httpServer.close();
}