Skip to content

Instantly share code, notes, and snippets.

@Richardn2002
Last active June 24, 2022 16:11
Show Gist options
  • Save Richardn2002/ef64d96ed7fc30043a8f13e30821b72b to your computer and use it in GitHub Desktop.
Save Richardn2002/ef64d96ed7fc30043a8f13e30821b72b to your computer and use it in GitHub Desktop.
Request HTTPS Certificate without IPv4 and port 80

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.

The .pem and the .key

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ssl-cert-snakeoil.key -out ssl-cert-snakeoil.pem

The Responder

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 Initiater

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”

Finally

./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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment