dehydrated-io / dehydrated

letsencrypt/acme client implemented as a shell-script – just add water
https://dehydrated.io
MIT License
5.96k stars 716 forks source link

Incorrect validation certificate for tls-alpn-01 challenge #911

Closed toasterparty closed 1 year ago

toasterparty commented 1 year ago

I'm trying to get my first cert for my first domain, toasterparty.net. As I understand, tls-alpn-01 via dehydrated are the best tools for the job since my ISP blocks port 80. I'm running Debian 11 in a headless configuration on standalone hardware. This error is what's stopping me from progressing:

urn:ietf:params:acme:error:unauthorized
Incorrect validation certificate for tls-alpn-01 challenge. Requested toasterparty.net from 72.197.16.252:443. Received certificate with unexpected extensions: "Required extension OID 1.3.6.1.5.5.7.1.31 is not present"

Script

This is how I'm using dehydrated.

#!/usr/bin/env bash
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CONFIG_DIR=$SCRIPT_DIR/config
cd $SCRIPT_DIR
mkdir -p $CONFIG_DIR
mkdir -p $SCRIPT_DIR/data/www/dehydrated/

# Start an SSL client to serve challenge data
sudo pkill -f alpn-responder.py
sudo python3 alpn-responder.py &
sleep 1

# Configure dehydrated
DEHYDRATED_VER=master
DEHYDRATED=$SCRIPT_DIR/data/dehydrated
wget -nv -nc https://raw.githubusercontent.com/dehydrated-io/dehydrated/$DEHYDRATED_VER/docs/examples/config -O $CONFIG_DIR/config.conf || true
wget -nv https://raw.githubusercontent.com/dehydrated-io/dehydrated/$DEHYDRATED_VER/dehydrated -O $DEHYDRATED
chmod +x $DEHYDRATED

# Request Certificate
sudo $DEHYDRATED --register --accept-terms -f $CONFIG_DIR/config.conf --domain toasterparty.net
sudo $DEHYDRATED -c -f $CONFIG_DIR/config.conf --domain toasterparty.net

# Cleanup
sudo pkill -f alpn-responder.py

Error Log

This is the log of a full run:

2023-05-13 14:10:28 URL:https://raw.githubusercontent.com/dehydrated-io/dehydrated/master/dehydrated [88973/88973] -> "/home/toaster/git/toaster-server/services/nginx/ssl-certs/data/dehydrated" [1]
# INFO: Using main config file /home/toaster/git/toaster-server/services/nginx/ssl-certs/config/config.conf
+ Account already registered!
# INFO: Using main config file /home/toaster/git/toaster-server/services/nginx/ssl-certs/config/config.conf
Processing toasterparty.net
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting new certificate order from CA...
 + Received 1 authorizations URLs from the CA
 + Handling authorization for toasterparty.net
 + Generating ALPN certificate and key for toasterparty.net...
 + 1 pending challenge(s)
 + Deploying challenge tokens...
 + Responding to challenge for toasterparty.net authorization...
Got request for toasterparty.net
Got request for toasterparty.net
Got request for toasterparty.net
 + Cleaning challenge tokens...
 + Challenge validation has failed :(
ERROR: Challenge is invalid! (returned: invalid) (result: ["type"]      "tls-alpn-01"
["status"]      "invalid"
["error","type"]        "urn:ietf:params:acme:error:unauthorized"
["error","detail"]      "Incorrect validation certificate for tls-alpn-01 challenge. Requested toasterparty.net from 72.197.16.252:443. Received certificate with unexpected extensions: \"Required extension OID 1.3.6.1.5.5.7.1.31 is not present\""
["error","status"]      403
["error"]       {"type":"urn:ietf:params:acme:error:unauthorized","detail":"Incorrect validation certificate for tls-alpn-01 challenge. Requested toasterparty.net from 72.197.16.252:443. Received certificate with unexpected extensions: \"Required extension OID 1.3.6.1.5.5.7.1.31 is not present\"","status":403}
["url"] "https://acme-v02.api.letsencrypt.org/acme/chall-v3/227519890297/yQrYAw"
["token"]       "--ze3nEpYDbpWNC-ncSv2V-_6GTo5sRUlc7oP7ZbUag"
["validationRecord",0,"hostname"]       "toasterparty.net"
["validationRecord",0,"port"]   "443"
["validationRecord",0,"addressesResolved",0]    "72.197.16.252"
["validationRecord",0,"addressesResolved"]      ["72.197.16.252"]
["validationRecord",0,"addressUsed"]    "72.197.16.252"
["validationRecord",0]  {"hostname":"toasterparty.net","port":"443","addressesResolved":["72.197.16.252"],"addressUsed":"72.197.16.252"}
["validationRecord"]    [{"hostname":"toasterparty.net","port":"443","addressesResolved":["72.197.16.252"],"addressUsed":"72.197.16.252"}]
["validated"]   "2023-05-13T21:10:31Z")

dehydrated -e

These are my environment vars after loading /config/config.conf

# dehydrated configuration
# INFO: Using main config file /home/toaster/git/toaster-server/services/nginx/ssl-certs/config/config.conf
declare -- CA="https://acme-v02.api.letsencrypt.org/directory"
declare -- CERTDIR="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/certs"
declare -- ALPNCERTDIR="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/alpn-certs"
declare -- CHALLENGETYPE="tls-alpn-01"
declare -- DOMAINS_D=""
declare -- DOMAINS_TXT="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../../config/domains.conf"
declare -- HOOK=""
declare -- HOOK_CHAIN="no"
declare -- RENEW_DAYS="30"
declare -- ACCOUNT_KEY="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/accounts/aHR0cHM6Ly9hY21lLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2RpcmVjdG9yeQo/account_key.pem"
declare -- ACCOUNT_KEY_JSON="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/accounts/aHR0cHM6Ly9hY21lLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2RpcmVjdG9yeQo/registration_info.json"
declare -- ACCOUNT_ID_JSON="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/accounts/aHR0cHM6Ly9hY21lLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2RpcmVjdG9yeQo/account_id.json"
declare -- KEYSIZE="4096"
declare -- WELLKNOWN="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/www/dehydrated"
declare -- PRIVATE_KEY_RENEW="yes"
declare -- OPENSSL_CNF="/usr/lib/ssl/openssl.cnf"
declare -- CONTACT_EMAIL=""
declare -- LOCKFILE="/home/toaster/git/toaster-server/services/nginx/ssl-certs/config/../data/lock"

Let's Debug

Here's proof my domain records are correct:

image

Checking for TLS 1.3

Here's proof my machine can handle TLS 1.3:

echo | openssl s_client -tls1_3 -connect tls13.cloudflare.com:443
openssl version
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 256 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
DONE
OpenSSL 1.1.1n  15 Mar 2022

alpn-responder.py

Here's the responder I'm using. It's the same as the provided example but using port 443 and ALPNDIR is set to match the environment variable.

#!/usr/bin/env python3

import ssl
import socketserver
import re
import os

ALPNDIR="/data/alpn-certs"
PROXY_PROTOCOL=False

FALLBACK_CERTIFICATE="/etc/ssl/certs/ssl-cert-snakeoil.pem"
FALLBACK_KEY="/etc/ssl/private/ssl-cert-snakeoil.key"

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

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 = "0.0.0.0", 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()

Thanks for any time spent looking into this!

toasterparty commented 1 year ago

To rule out file permission issues, I've already tried:

sudo chown -R toaster .
sudo chmod -R 777 .

I've also tried calling dehydrated and/or the responder with and without sudo

lukas2511 commented 1 year ago

Let's Debug doesn't really seem to check for the required certificate so that output is kinda useless.

Just to make sure the alpn validation certs are generated correctly maybe try running openssl x509 -noout -text -in path/to/alpn/cert (you might need to kill dehydrated before it deletes the generated cert, maybe Ctrl+c works too, can't check that right now). The option mentioned in the error message should be somewhere in there.

If you can see the option in the certificate I'd guess that there is still something either running on your machine and listening on port 443, or there's something in the path between Let's Encrypt and you, maybe your provider is also running some weird firewall that actually blocks the request or something like that.

toasterparty commented 1 year ago

I figured it out!

The problem is simple, but the error is misleading:

In my responder I had:

ALPNDIR="/data/alpn-certs"

When I needed:

ALPNDIR="./data/alpn-certs"

This edit to load_certificate made it super obvious what was happening: image

toasterparty commented 1 year ago

(and for the record you can examine the certificate with a well-timed CTRL+C)