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:
parent
fa8814baab
commit
9ea4a54ca2
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")],
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue