Skip to content

Commit

Permalink
Add support for IP certificates (#13)
Browse files Browse the repository at this point in the history
* Add support for IP certificates.

* Bump Pebble version.

* Bump Pebble version again.

* Fix identifier detection/adding.
  • Loading branch information
felixfontein authored and mattclay committed Jun 7, 2019
1 parent ed26a86 commit 7c165d1
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM golang:1.10-stretch as builder
# Install pebble
ARG PEBBLE_CHECKOUT="1f851bf3f6b0c22d2f59c1e8958b79e0ab7ad580"
ARG PEBBLE_CHECKOUT="7228963479dd2bce0c040049b18e67393155bc6a"
ENV GOPATH=/go
RUN go get -u github.com/letsencrypt/pebble/... && \
cd /go/src/github.com/letsencrypt/pebble && \
Expand Down
16 changes: 12 additions & 4 deletions acme_tlsalpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ def _cert_selection(self, connection):
server_name = connection.get_servername()
self.log_callback("TLS ALPN Challenge server: Serving challenge cert for server name {0}".format(server_name))
# return self.certs.get(server_name, None)
return self.challenge_certs.get(server_name, None)
if server_name.endswith(b'.'):
server_name = server_name[:-1]
return self.challenge_certs.get(server_name)

def _alpn_selection(self, _connection, alpn_protos):
"""Callback to select alpn protocol."""
Expand Down Expand Up @@ -198,11 +200,15 @@ def __init__(self, port, log_callback):
self.log_callback = log_callback

def add(self, domain, key, cert_normal, cert_challenge):
if domain.endswith('.'):
domain = domain[:-1]
domain = domain.encode('utf-8')
self.certs[domain] = (key, cert_normal)
self.challenge_certs[domain] = (key, cert_challenge)

def remove(self, domain):
if domain.endswith('.'):
domain = domain[:-1]
domain = domain.encode('utf-8')
self.certs.pop(domain)
self.challenge_certs.pop(domain)
Expand All @@ -216,14 +222,16 @@ def update(self):
self.thread.start()


def gen_ss_cert(key, domains, extensions):
def gen_ss_cert(key, domains, ips, extensions):
cert = crypto.X509()
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
cert.set_version(2)
extensions.append(crypto.X509Extension(b"basicConstraints", True, b"CA:TRUE, pathlen:0"))
cert.get_subject().CN = domains[0]
cert.set_issuer(cert.get_subject())
extensions.append(crypto.X509Extension(b"subjectAltName", critical=False, value=b", ".join(b"DNS:" + d.encode() for d in domains)))
sans = []
sans.extend([b"DNS:" + d.encode() for d in domains])
sans.extend([b"IP:" + d.encode() for d in ips])
extensions.append(crypto.X509Extension(b"subjectAltName", critical=False, value=b", ".join(sans)))
cert.add_extensions(extensions)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(24 * 60 * 60)
Expand Down
41 changes: 26 additions & 15 deletions controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,20 @@ def dns_challenge(record):
tls_alpn_server = ALPNChallengeServer(port=5001, log_callback=log)


def _get_alpn_key_cert_from_der_value(domain, data):
def _get_alpn_key_cert_from_der_value(domain, identifier, data):
der_value = b"DER:0420" + codecs.encode(base64.standard_b64decode(data), 'hex')
domains = []
ips = []
if identifier.upper().startswith('DNS:'):
domains.append(identifier[4:])
elif identifier.upper().startswith('IP:'):
ips.append(identifier[3:])
# Create private key
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
# Create self-signed certificates
acme_extension = crypto.X509Extension(b"1.3.6.1.5.5.7.1.31", critical=True, value=der_value)
cert_challenge = gen_ss_cert(key, [domain], extensions=[acme_extension])
cert_challenge = gen_ss_cert(key, domains, ips, extensions=[acme_extension])
return key, cert_challenge


Expand All @@ -144,7 +150,7 @@ def _find_line_regex(lines, regex):
raise Exception('Cannot find line in input which matches "{0}"!'.format(regex))


def _get_alpn_key_cert_from_pem_chain(domain, data):
def _get_alpn_key_cert_from_pem_chain(domain, identifier, data):
data = data.split(b'\n')
# Extract challenge certificate
cert_lines = data[_find_line_regex(data, b'-----BEGIN .*CERTIFICATE-----'):_find_line_regex(data, b'-----END .*CERTIFICATE-----') + 1]
Expand All @@ -155,22 +161,22 @@ def _get_alpn_key_cert_from_pem_chain(domain, data):
return key, cert_challenge


@app.route('/tls-alpn/<string:domain>/der-value-b64', methods=['PUT'])
def tls_alpn_challenge_put_b64(domain):
log('Adding TLS ALPN challenge for domain {0} (Base64 encoded DER value)'.format(domain))
key, cert_challenge = _get_alpn_key_cert_from_der_value(domain, request.data)
cert_normal = gen_ss_cert(key, [domain], [])
@app.route('/tls-alpn/<string:domain>/<string:identifier>/der-value-b64', methods=['PUT'])
def tls_alpn_challenge_put_b64(domain, identifier):
log('Adding TLS ALPN challenge for domain {0} and identifier {1} (Base64 encoded DER value)'.format(domain, identifier))
key, cert_challenge = _get_alpn_key_cert_from_der_value(domain, identifier, request.data)
cert_normal = gen_ss_cert(key, [domain], [], [])
# Start/modify TLS-ALPN-01 challenge server
tls_alpn_server.add(domain, key, cert_normal, cert_challenge)
tls_alpn_server.update()
return 'ok'


@app.route('/tls-alpn/<string:domain>/certificate-and-key', methods=['PUT'])
def tls_alpn_challenge_put_pem(domain):
log('Adding TLS ALPN challenge for domain {0} (PEM certificate and key)'.format(domain))
key, cert_challenge = _get_alpn_key_cert_from_pem_chain(domain, request.data)
cert_normal = gen_ss_cert(key, [domain], [])
@app.route('/tls-alpn/<string:domain>/<string:identifier>/certificate-and-key', methods=['PUT'])
def tls_alpn_challenge_put_pem(domain, identifier):
log('Adding TLS ALPN challenge for domain {0} and identifier {1} (PEM certificate and key)'.format(domain, identifier))
key, cert_challenge = _get_alpn_key_cert_from_pem_chain(domain, identifier, request.data)
cert_normal = gen_ss_cert(key, [domain], [], [])
# Start/modify TLS-ALPN-01 challenge server
tls_alpn_server.add(domain, key, cert_normal, cert_challenge)
tls_alpn_server.update()
Expand All @@ -188,9 +194,14 @@ def tls_alpn_challenge_delete(domain):
@app.route('/.well-known/acme-challenge/<string:filename>')
def get_http_challenge(filename):
host = request.headers.get('Host')
i = host.find(':')
if host.startswith('[') and ']' in host:
i = host.find(':', host.find(']'))
else:
i = host.find(':')
if i >= 0:
host = host[:i]
if host[0] == '[' and host[-1] == ']':
host = host[1:-1]
if host not in challenges:
log('Retrieving HTTP challenge for unknown host {0}!'.format(host))
return 'unknown host', 404
Expand All @@ -216,4 +227,4 @@ def get_root_certificate_pebble():


if __name__ == "__main__":
app.run(debug=False, host='0.0.0.0', port=int(os.environ.get('CONTROLLER_PORT', 5000)))
app.run(debug=False, host='::', port=int(os.environ.get('CONTROLLER_PORT', 5000)))

0 comments on commit 7c165d1

Please sign in to comment.