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

Closed
opened 2025-12-29 01:27:46 +01:00 by adam · 4 comments
Owner

Originally created by @toasterparty on GitHub (May 14, 2023).

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!

Originally created by @toasterparty on GitHub (May 14, 2023). 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. ```bash #!/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` ```bash # 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](https://github.com/dehydrated-io/dehydrated/assets/14116556/abfc560d-449b-4603-8e90-3871611eb7e2) # 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. ```py #!/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!
adam closed this issue 2025-12-29 01:27:46 +01:00
Author
Owner

@toasterparty commented on GitHub (May 14, 2023):

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

@toasterparty commented on GitHub (May 14, 2023): 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`
Author
Owner

@lukas2511 commented on GitHub (May 14, 2023):

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.

@lukas2511 commented on GitHub (May 14, 2023): 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.
Author
Owner

@toasterparty commented on GitHub (May 15, 2023):

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 on GitHub (May 15, 2023): 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](https://github.com/dehydrated-io/dehydrated/assets/14116556/f48f68f4-4f77-4f90-babd-0bbb55a01ec5)
Author
Owner

@toasterparty commented on GitHub (May 15, 2023):

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

@toasterparty commented on GitHub (May 15, 2023): (and for the record you can examine the certificate with a well-timed CTRL+C)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/dehydrated#605