To not use port 80, the only challenge mode left is TLS-ALPN-01 (WRONG! We still have DNS Validation. Seems like my knowledge base is a bit outdated. But at least this is a cute little automated method.). The process involves:
- A pair of self-signed SSL certificate and key.
- An ALPN challenge responder.
- A certificate request initiater.
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ssl-cert-snakeoil.key -out ssl-cert-snakeoil.pem
The original code is from here, but a little modification is required to enable support for IPv6.
alpn-responder.py
:
#!/usr/bin/env python3
import ssl
import socketserver, socket
import threading
import re
import os
ALPNDIR="<YOUR PATH HERE>/alpn-certs"
PROXY_PROTOCOL=False
FALLBACK_CERTIFICATE="<YOUR PATH HERE>/ssl-cert-snakeoil.pem"
FALLBACK_KEY="<YOUR PATH HERE>/ssl-cert-snakeoil.key"
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
address_family = socket.AF_INET6
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def create_context(self, certfile, keyfile, first=False):
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.set_ciphers('ECDHE+AESGCM')
ssl_context.set_alpn_protocols(["acme-tls/1"])
ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
if first:
ssl_context.set_servername_callback(self.load_certificate)
ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
return ssl_context
def load_certificate(self, sslsocket, sni_name, sslcontext):
print("Got request for %s" % sni_name)
if not re.match(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$', sni_name):
return
certfile = os.path.join(ALPNDIR, "%s.crt.pem" % sni_name)
keyfile = os.path.join(ALPNDIR, "%s.key.pem" % sni_name)
if not os.path.exists(certfile) or not os.path.exists(keyfile):
return
sslsocket.context = self.create_context(certfile, keyfile)
def handle(self):
if PROXY_PROTOCOL:
buf = b""
while b"\r\n" not in buf:
buf += self.request.recv(1)
ssl_context = self.create_context(FALLBACK_CERTIFICATE, FALLBACK_KEY, True)
newsock = ssl_context.wrap_socket(self.request, server_side=True)
if __name__ == "__main__":
HOST, PORT = "::", 443
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler, bind_and_activate=False)
server.allow_reuse_address = True
try:
server.server_bind()
server.server_activate()
server.serve_forever()
except:
server.shutdown()
The implementation I use is dehydrated and the reference is this page.
wget https://raw.githubusercontent.com/lukas2511/dehydrated/master/dehydrated
wget https://raw.githubusercontent.com/lukas2511/dehydrated/master/docs/examples/config
wget https://raw.githubusercontent.com/lukas2511/dehydrated/master/docs/examples/domains.txt
chmod +x dehydrated
Modify domains.txt
to input the desired domain names and modify config
at the following entries:
CHALLENGETYPE=”tls-alpn-01"
ALPNCERTDIR=”<YOUR PATH HERE>/alpn-certs”
CERTDIR=”<YOUR PATH HERE>/certs”
./dehydrated --register --accept-terms
python3 alpn-responder.py
./dehydrated -c -f config
If challenge fails with error type connection
and code 400
, check your firewall and make sure the outer world can access your device.
Remember to shutdown the little Python server when everything is done. It will continue listening for challenges till randomly crashing.