Skip to content

Commit bb9ddb1

Browse files
mdebskicpu
authored andcommitted
Implement TLS-ALPN-01 and integration test for it (letsencrypt#3654)
This implements newly proposed TLS-ALPN-01 validation method, as described in https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01 This challenge type is disabled except in the config-next tree.
1 parent 63f34c3 commit bb9ddb1

File tree

15 files changed

+392
-58
lines changed

15 files changed

+392
-58
lines changed

core/challenges.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ func TLSSNIChallenge01() Challenge {
2222
func DNSChallenge01() Challenge {
2323
return newChallenge(ChallengeTypeDNS01)
2424
}
25+
26+
// TLSALPNChallenge01 constructs a random tls-alpn-01 challenge
27+
func TLSALPNChallenge01() Challenge {
28+
return newChallenge(ChallengeTypeTLSALPN01)
29+
}

core/core_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ func TestChallenges(t *testing.T) {
3333
dns01 := DNSChallenge01()
3434
test.AssertNotError(t, dns01.CheckConsistencyForClientOffer(), "CheckConsistencyForClientOffer returned an error")
3535

36+
tlsalpn01 := TLSALPNChallenge01()
37+
test.AssertNotError(t, tlsalpn01.CheckConsistencyForClientOffer(), "CheckConsistencyForClientOffer returned an error")
38+
3639
test.Assert(t, ValidChallenge(ChallengeTypeHTTP01), "Refused valid challenge")
3740
test.Assert(t, ValidChallenge(ChallengeTypeTLSSNI01), "Refused valid challenge")
3841
test.Assert(t, ValidChallenge(ChallengeTypeDNS01), "Refused valid challenge")
42+
test.Assert(t, ValidChallenge(ChallengeTypeTLSALPN01), "Refused valid challenge")
3943
test.Assert(t, !ValidChallenge("nonsense-71"), "Accepted invalid challenge")
4044
}
4145

core/objects.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,20 @@ const (
6868

6969
// These types are the available challenges
7070
const (
71-
ChallengeTypeHTTP01 = "http-01"
72-
ChallengeTypeTLSSNI01 = "tls-sni-01"
73-
ChallengeTypeDNS01 = "dns-01"
71+
ChallengeTypeHTTP01 = "http-01"
72+
ChallengeTypeTLSSNI01 = "tls-sni-01"
73+
ChallengeTypeDNS01 = "dns-01"
74+
ChallengeTypeTLSALPN01 = "tls-alpn-01"
7475
)
7576

7677
// ValidChallenge tests whether the provided string names a known challenge
7778
func ValidChallenge(name string) bool {
7879
switch name {
79-
case ChallengeTypeHTTP01:
80-
fallthrough
81-
case ChallengeTypeTLSSNI01:
82-
fallthrough
83-
case ChallengeTypeDNS01:
80+
case ChallengeTypeHTTP01,
81+
ChallengeTypeTLSSNI01,
82+
ChallengeTypeDNS01,
83+
ChallengeTypeTLSALPN01:
8484
return true
85-
8685
default:
8786
return false
8887
}
@@ -233,7 +232,7 @@ type Challenge struct {
233232
// For the V2 API the "URI" field is deprecated in favour of URL.
234233
URL string `json:"url,omitempty"`
235234

236-
// Used by http-01, tls-sni-01, and dns-01 challenges
235+
// Used by http-01, tls-sni-01, tls-alpn-01 and dns-01 challenges
237236
Token string `json:"token,omitempty"`
238237

239238
// The expected KeyAuthorization for validation of the challenge. Populated by
@@ -279,7 +278,7 @@ func (ch Challenge) RecordsSane() bool {
279278
return false
280279
}
281280
}
282-
case ChallengeTypeTLSSNI01:
281+
case ChallengeTypeTLSSNI01, ChallengeTypeTLSALPN01:
283282
if len(ch.ValidationRecord) > 1 {
284283
return false
285284
}

core/objects_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func TestChallengeSanityCheck(t *testing.T) {
5757
}`), &accountKey)
5858
test.AssertNotError(t, err, "Error unmarshaling JWK")
5959

60-
types := []string{ChallengeTypeHTTP01, ChallengeTypeTLSSNI01, ChallengeTypeDNS01}
60+
types := []string{ChallengeTypeHTTP01, ChallengeTypeTLSSNI01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01}
6161
for _, challengeType := range types {
6262
chall := Challenge{
6363
Type: challengeType,

policy/pa.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ func (pa *AuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, regID int
445445
challenges = append(challenges, core.TLSSNIChallenge01())
446446
}
447447

448+
if pa.ChallengeTypeEnabled(core.ChallengeTypeTLSALPN01, regID) {
449+
challenges = append(challenges, core.TLSALPNChallenge01())
450+
}
451+
448452
if pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01, regID) {
449453
challenges = append(challenges, core.DNSChallenge01())
450454
}

test/PKI.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ Intermediate 1 (happy hacker fake CA):
4242

4343
Intermediate 2 (h2ppy h2cker fake CA):
4444
test-ca2.key test-ca2.pem
45+
46+
Certificate test-example.pem, together with test-example.key are self-signed
47+
certs used in tests. They were generated using:
48+
openssl req -x509 -newkey rsa:4096 -keyout test-example.key -out test-example.pem -days 36500 -nodes -subj "/CN=www.example.com"

test/chisel.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
import json
1111
import logging
1212
import os
13+
import socket
1314
import sys
1415
import threading
1516
import time
1617
import urllib2
1718

19+
from cryptography import x509
1820
from cryptography.hazmat.backends import default_backend
1921
from cryptography.hazmat.primitives.asymmetric import rsa
22+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
2023

2124
import OpenSSL
25+
from OpenSSL import SSL
2226
import josepy
2327

2428
from acme import challenges
@@ -144,6 +148,12 @@ def http_01_answer(client, chall_body):
144148
chall=chall_body.chall, response=response,
145149
validation=validation)
146150

151+
def tls_alpn_01_cert(client, chall_body, domain):
152+
"""Return x509 certificate for tls-alpn-01 challenge"""
153+
response = chall_body.response(client.key)
154+
cert, key = response.gen_cert(domain)
155+
return key, cert
156+
147157
def do_dns_challenges(client, authzs):
148158
cleanup_hosts = []
149159
for a in authzs:
@@ -190,6 +200,53 @@ def cleanup():
190200
thread.join()
191201
return cleanup
192202

203+
def do_tlsalpn_challenges(client, authzs):
204+
port = 5001
205+
example_key, example_cert = load_example_cert()
206+
server_certs = {'localhost': (example_key, example_cert)}
207+
challs = {a.body.identifier.value: get_chall(a, challenges.TLSALPN01)
208+
for a in authzs}
209+
chall_certs = {domain: tls_alpn_01_cert(client, c, domain)
210+
for domain, c in challs.items()}
211+
# TODO: this won't be needed once acme standalone tls-alpn server serves
212+
# certs correctly, not only challenge certs.
213+
chall_certs['localhost'] = (example_key, example_cert)
214+
server = standalone.TLSALPN01Server(("", port), server_certs, chall_certs)
215+
thread = threading.Thread(target=server.serve_forever)
216+
thread.start()
217+
218+
# Loop until the TLSALPN01Server is ready.
219+
while True:
220+
try:
221+
s = socket.socket()
222+
s.connect(("localhost", port))
223+
client_ssl = SSL.Connection(SSL.Context(SSL.TLSv1_METHOD), s)
224+
client_ssl.set_connect_state()
225+
client_ssl.set_tlsext_host_name("localhost")
226+
client_ssl.set_alpn_protos([b'acme-tls/1'])
227+
client_ssl.do_handshake()
228+
break
229+
except (socket.error, SSL.Error):
230+
time.sleep(0.1)
231+
finally:
232+
s.close()
233+
234+
for chall_body in challs.values():
235+
client.answer_challenge(chall_body, chall_body.response(client.key))
236+
237+
def cleanup():
238+
server.shutdown()
239+
server.server_close()
240+
thread.join()
241+
return cleanup
242+
243+
def load_example_cert():
244+
keypem = open('test/test-example.key', 'rb').read()
245+
key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, keypem)
246+
crtpem = open('test/test-example.pem', 'rb').read()
247+
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, crtpem)
248+
return (key, cert)
249+
193250
def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, client=None):
194251
"""Make authzs for each of the given domains, set up a server to answer the
195252
challenges in those authzs, tell the ACME server to validate the challenges,
@@ -202,6 +259,8 @@ def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, c
202259
cleanup = do_http_challenges(client, authzs)
203260
elif chall_type == "dns-01":
204261
cleanup = do_dns_challenges(client, authzs)
262+
elif chall_type == "tls-alpn-01":
263+
cleanup = do_tlsalpn_challenges(client, authzs)
205264
else:
206265
raise Exception("invalid challenge type %s" % chall_type)
207266

test/config-next/ca.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@
142142
"challenges": {
143143
"http-01": true,
144144
"tls-sni-01": true,
145-
"dns-01": true
145+
"dns-01": true,
146+
"tls-alpn-01": true
146147
}
147148
},
148149

test/config-next/cert-checker.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"challenges": {
1010
"http-01": true,
1111
"tls-sni-01": true,
12-
"dns-01": true
12+
"dns-01": true,
13+
"tls-alpn-01": true
1314
}
1415
},
1516

test/config-next/ra.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@
9999
"challenges": {
100100
"http-01": true,
101101
"tls-sni-01": true,
102-
"dns-01": true
102+
"dns-01": true,
103+
"tls-alpn-01": true
103104
},
104105
"challengesWhitelistFile": "test/challenges-whitelist.json"
105106
},

0 commit comments

Comments
 (0)