Merge pull request #1277 from letsencrypt/dns-integration
Enable DNS challenge integration tests
This commit is contained in:
commit
f3d368eaff
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue