Use challtestsrv for solving TLS-ALPN-01 in integration tests (#3789)

Also in the process fix some errors I made in the original challtestsrv TLS-ALPN-01 implementation.

Fixes #3780.
This commit is contained in:
Roland Bracewell Shoemaker 2018-07-03 07:41:20 -07:00 committed by Daniel McCarney
parent fa8814baab
commit 9ea4a54ca2
7 changed files with 136 additions and 48 deletions

View File

@ -58,6 +58,8 @@ func main() {
"Comma separated bind addresses/ports for HTTP-01 challenges. Set empty to disable.")
dnsOneBind := flag.String("dns01", ":8053",
"Comma separated bind addresses/ports for DNS-01 challenges and fake DNS data. Set empty to disable.")
tlsAlpnOneBind := flag.String("tlsalpn01", ":5001",
"Comma separated bind addresses/ports for TLS-ALPN-01 challenges. Set empty to disable.")
managementBind := flag.String("management", ":8055",
"Bind address/port for management HTTP interface")
@ -65,14 +67,16 @@ func main() {
httpOneAddresses := filterEmpty(strings.Split(*httpOneBind, ","))
dnsOneAddresses := filterEmpty(strings.Split(*dnsOneBind, ","))
tlsAlpnOneAddresses := filterEmpty(strings.Split(*tlsAlpnOneBind, ","))
logger := log.New(os.Stdout, "challtestsrv - ", log.Ldate|log.Ltime)
// Create a new challenge server with the provided config
srv, err := challtestsrv.New(challtestsrv.Config{
HTTPOneAddrs: httpOneAddresses,
DNSOneAddrs: dnsOneAddresses,
Log: logger,
HTTPOneAddrs: httpOneAddresses,
DNSOneAddrs: dnsOneAddresses,
TLSALPNOneAddrs: tlsAlpnOneAddresses,
Log: logger,
})
cmd.FailOnError(err, "Unable to construct challenge server")
@ -94,6 +98,10 @@ func main() {
http.HandleFunc("/set-txt", oobSrv.addDNS01)
http.HandleFunc("/clear-txt", oobSrv.delDNS01)
}
if *tlsAlpnOneBind != "" {
http.HandleFunc("/add-tlsalpn01", oobSrv.addTLSALPN01)
http.HandleFunc("/del-tlsalpn01", oobSrv.delTLSALPN01)
}
// Start all of the sub-servers in their own Go routines so that the main Go
// routine can spin forever looking for signals to catch.

View File

@ -0,0 +1,73 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
)
// addTLSALPN01 handles an HTTP POST request to add a new TLS-ALPN-01 challenge
// response for a given host.
func (srv *managementServer) addTLSALPN01(w http.ResponseWriter, r *http.Request) {
// Read the request body
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Unmarshal the request body JSON as a request object
var request struct {
Host string
Content string
}
err = json.Unmarshal(msg, &request)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// If the request has an empty host or content it's a bad request
if request.Host == "" || request.Content == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
// Add the TLS-ALPN-01 challenge to the challenge server
srv.challSrv.AddTLSALPNChallenge(request.Host, request.Content)
srv.log.Printf("Added TLS-ALPN-01 challenge for host %q - key auth %q\n",
request.Host, request.Content)
w.WriteHeader(http.StatusOK)
}
// delTLSALPN01 handles an HTTP POST request to delete an existing TLS-ALPN-01
// challenge response for a given host.
func (srv *managementServer) delTLSALPN01(w http.ResponseWriter, r *http.Request) {
// Read the request body
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Unmarshal the request body JSON as a request object
var request struct {
Host string
}
err = json.Unmarshal(msg, &request)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// If the request has an empty host it's a bad request
if request.Host == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
// Delete the TLS-ALPN-01 challenge for the given host from the challenge server
srv.challSrv.DeleteTLSALPNChallenge(request.Host)
srv.log.Printf("Removed TLS-ALPN-01 challenge for host %q\n", request.Host)
w.WriteHeader(http.StatusOK)
}

View File

@ -11,6 +11,7 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"math/big"
"net/http"
"time"
@ -60,8 +61,9 @@ func (s *ChallSrv) ServeChallengeCertFunc(k *ecdsa.PrivateKey) func(*tls.ClientH
return nil, fmt.Errorf("failed marshalling hash OCTET STRING: %s", err)
}
certTmpl := x509.Certificate{
DNSNames: []string{hello.ServerName},
Extensions: []pkix.Extension{
SerialNumber: big.NewInt(1729),
DNSNames: []string{hello.ServerName},
ExtraExtensions: []pkix.Extension{
{
Id: va.IdPeAcmeIdentifierV1,
Critical: true,

View File

@ -40,6 +40,8 @@ DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4000/directory')
# URLs for management interface of challtestsrv
SET_TXT = "http://localhost:8055/set-txt"
CLEAR_TXT = "http://localhost:8055/clear-txt"
ADD_ALPN = "http://localhost:8055/add-tlsalpn01"
DEL_ALPN = "http://localhost:8055/del-tlsalpn01"
os.environ.setdefault('REQUESTS_CA_BUNDLE', 'test/wfe-tls/minica.pem')
@ -148,12 +150,6 @@ def http_01_answer(client, chall_body):
chall=chall_body.chall, response=response,
validation=validation)
def tls_alpn_01_cert(client, chall_body, domain):
"""Return x509 certificate for tls-alpn-01 challenge"""
response = chall_body.response(client.key)
cert, key = response.gen_cert(domain)
return key, cert
def do_dns_challenges(client, authzs):
cleanup_hosts = []
for a in authzs:
@ -201,43 +197,23 @@ def do_http_challenges(client, authzs):
return cleanup
def do_tlsalpn_challenges(client, authzs):
port = 5001
example_key, example_cert = load_example_cert()
server_certs = {'localhost': (example_key, example_cert)}
challs = {a.body.identifier.value: get_chall(a, challenges.TLSALPN01)
for a in authzs}
chall_certs = {domain: tls_alpn_01_cert(client, c, domain)
for domain, c in challs.items()}
# TODO: this won't be needed once acme standalone tls-alpn server serves
# certs correctly, not only challenge certs.
chall_certs['localhost'] = (example_key, example_cert)
server = standalone.TLSALPN01Server(("", port), server_certs, chall_certs)
thread = threading.Thread(target=server.serve_forever)
thread.start()
# Loop until the TLSALPN01Server is ready.
while True:
try:
s = socket.socket()
s.connect(("localhost", port))
client_ssl = SSL.Connection(SSL.Context(SSL.TLSv1_METHOD), s)
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name("localhost")
client_ssl.set_alpn_protos([b'acme-tls/1'])
client_ssl.do_handshake()
break
except (socket.error, SSL.Error):
time.sleep(0.1)
finally:
s.close()
for chall_body in challs.values():
client.answer_challenge(chall_body, chall_body.response(client.key))
cleanup_hosts = []
for a in authzs:
c = get_chall(a, challenges.TLSALPN01)
name, value = (a.body.identifier.value, c.key_authorization(client.key))
cleanup_hosts.append(name)
urllib2.urlopen(ADD_ALPN,
data=json.dumps({
"host": name,
"content": value,
})).read()
client.answer_challenge(c, c.response(client.key))
def cleanup():
server.shutdown()
server.server_close()
thread.join()
for host in cleanup_hosts:
urllib2.urlopen(DEL_ALPN,
data=json.dumps({
"host": host,
})).read()
return cleanup
def load_example_cert():

View File

@ -45,6 +45,8 @@ os.environ.setdefault('REQUESTS_CA_BUNDLE', 'test/wfe-tls/minica.pem')
# URLs for management interface of challtestsrv
SET_TXT = "http://localhost:8055/set-txt"
CLEAR_TXT = "http://localhost:8055/clear-txt"
ADD_ALPN = "http://localhost:8055/add-tlsalpn01"
DEL_ALPN = "http://localhost:8055/del-tlsalpn01"
def uninitialized_client(key=None):
if key is None:
@ -108,6 +110,8 @@ def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, c
cleanup = do_http_challenges(client, authzs)
elif chall_type == "dns-01":
cleanup = do_dns_challenges(client, authzs)
elif chall_type == "tls-alpn-01":
cleanup = do_tlsalpn_challenges(client, authzs)
else:
raise Exception("invalid challenge type %s" % chall_type)
@ -171,6 +175,26 @@ def do_http_challenges(client, authzs):
return cleanup
def do_tlsalpn_challenges(client, authzs):
cleanup_hosts = []
for a in authzs:
c = get_chall(a, challenges.TLSALPN01)
name, value = (a.body.identifier.value, c.key_authorization(client.key))
cleanup_hosts.append(name)
urllib2.urlopen(ADD_ALPN,
data=json.dumps({
"host": name,
"content": value,
})).read()
client.answer_challenge(c, c.response(client.key))
def cleanup():
for host in cleanup_hosts:
urllib2.urlopen(DEL_ALPN,
data=json.dumps({
"host": host,
})).read()
return cleanup
def expect_problem(problem_type, func):
"""Run a function. If it raises a ValidationError or messages.Error that
contains the given problem_type, return. If it raises no error or the wrong

View File

@ -92,7 +92,7 @@ def start(race_detection, fakeclock=None, account_uri=None):
# The gsb-test-srv needs to be started before the VA or its intial DB
# update will fail and all subsequent lookups will be invalid
[6000, 'gsb-test-srv -apikey my-voice-is-my-passport'],
[8053, 'challtestsrv --dns01 :8053,:8054 --management :8055 --http01 ""'],
[8053, 'challtestsrv --dns01 :8053,:8054 --management :8055 --http01 "" --tlsalpn01 :5001'],
[8004, 'boulder-va --config %s --addr va1.boulder:9092 --debug-addr :8004' % os.path.join(default_config_dir, "va.json")],
[8104, 'boulder-va --config %s --addr va2.boulder:9092 --debug-addr :8104' % os.path.join(default_config_dir, "va.json")],
[8001, 'boulder-ca --config %s --ca-addr ca1.boulder:9093 --ocsp-addr ca1.boulder:9096 --debug-addr :8001' % os.path.join(default_config_dir, "ca.json")],

View File

@ -41,6 +41,11 @@ def test_wildcardmultidomain():
def test_http_challenge():
chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="http-01")
def test_tls_alpn_challenge():
if not default_config_dir.startswith("test/config-next"):
return
chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="tls-alpn-01")
def test_overlapping_wildcard():
"""
Test issuance for a random domain and a wildcard version of the same domain