Add support for TLS-SNI-01 challenges #143

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

Originally created by @j-ed on GitHub (Sep 12, 2016).

At the moment letsencrypt.sh only supports challenges of the type http-01 or dns-01.
If http-01 is selected it is necessary that the server answers incoming validation requests on port 80/tcp. Due to the fact that more and more servers nowadays are using TLS on port 443/tcp by default without having port 80/tcp activated at all, it would be necessary to temporarily open port 80/tcp at least at the time you want to let Let's Encrypt verify a certificate request.
Therefore it would be nice if support for tls-sni-01 could be added, which from my understanding, used a TLS connection on port 443/tcp to validate a certificate request. I've found a detailed description of it in the following ACME document:

https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md
usage

Originally created by @j-ed on GitHub (Sep 12, 2016). At the moment letsencrypt.sh only supports challenges of the type `http-01` or `dns-01`. If `http-01` is selected it is necessary that the server answers incoming validation requests on port 80/tcp. Due to the fact that more and more servers nowadays are using TLS on port 443/tcp by default without having port 80/tcp activated at all, it would be necessary to temporarily open port 80/tcp at least at the time you want to let Let's Encrypt verify a certificate request. Therefore it would be nice if support for `tls-sni-01` could be added, which from my understanding, used a TLS connection on port 443/tcp to validate a certificate request. I've found a detailed description of it in the following ACME document: https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md usage
adam added the help wanted label 2025-12-29 00:25:46 +01:00
adam closed this issue 2025-12-29 00:25:46 +01:00
Author
Owner

@txr13 commented on GitHub (Sep 29, 2016):

I'm not sure about adding support for this sort of challenge in dehydrated itself. Creating the certificate with the required values is easy, but then you have to actually push that cert into some sort of web daemon process, to return the generated cert to the requesting CA. (This is in contrast to the http-01 challenges, for which we can just drop files into an existing directory served by the daemon.)

I could see, potentially, adding support for this challenge in the same way as dns-01 challenges are handled. That is, hand the request off to a hook script. Let the hook take care of generating the proper cert, stuffing it into the right config location for whatever daemon is handling web requests, and reloading the web daemon.

@txr13 commented on GitHub (Sep 29, 2016): I'm not sure about adding support for this sort of challenge in dehydrated itself. Creating the certificate with the required values is easy, but then you have to actually push that cert into some sort of web daemon process, to return the generated cert to the requesting CA. (This is in contrast to the http-01 challenges, for which we can just drop files into an existing directory served by the daemon.) I could see, potentially, adding support for this challenge in the same way as dns-01 challenges are handled. That is, hand the request off to a hook script. Let the hook take care of generating the proper cert, stuffing it into the right config location for whatever daemon is handling web requests, and reloading the web daemon.
Author
Owner

@j-ed commented on GitHub (Sep 30, 2016):

@txr13 I've spent some time to study the ACME document and tend to agree that it should be possible to implement the function without big effort.
Due to the fact that dehydrated could update more than one certificate in one go, the hook script need to be extended with pre- and post-deployment hooks, which are called once per script run to allow to stop e.g. a running web server, and start it again afterwards.
The deploy_challenge hook could be used to push the created cert into some sort of web daemon process. For that it would need to be extend by an additional environment variable CERT_FILENAME, the DOMAIN variable, already exists and can be used if necessary to set the domain name etc.
Am I missing something?

@j-ed commented on GitHub (Sep 30, 2016): @txr13 I've spent some time to study the ACME document and tend to agree that it should be possible to implement the function without big effort. Due to the fact that dehydrated could update more than one certificate in one go, the hook script need to be extended with pre- and post-deployment hooks, which are called once per script run to allow to stop e.g. a running web server, and start it again afterwards. The `deploy_challenge` hook could be used to push the created cert into some sort of web daemon process. For that it would need to be extend by an additional environment variable CERT_FILENAME, the DOMAIN variable, already exists and can be used if necessary to set the domain name etc. Am I missing something?
Author
Owner

@txr13 commented on GitHub (Sep 30, 2016):

@j-ed I'm not convinced that pre- and post-deployment hooks are needed.

Consider how the DNS challenges are currently deployed. All dehydrated has to do is pass off the domains and list of challenges. The hook script takes care of the details of generating the records and pushing it live, however that needs to happen.

For the tls-sni-02 challenges, it would be identical. The deploy_challenge function gets handed a list of domains and challenges (preferably using HOOK_CHAIN=yes). The hook script takes care of everything after that--including generating the challenge certificate, updating webserver config, and then calling the webserver's reload or restart function. You don't need any new variables or functions in the hooks at all.

From a practical standpoint, the hooking API (list of variables passed, etc.) should be kept identical, and behavior should be kept identical, too. It would be much more effort to essentially make two different hooking APIs (one for DNS, where just challenges are passed, and one for TLS, where you're passing a certificate of some sort).

No, I really think that pushing just the domains and challenge info as currently exists is the way to go. Of necessity, that means that the hook script has to handle the challenge certificate generation--but then the hook also has the opportunity to set permissions, certificate location, config updates, and start/stop/restart/reload functions however you please. (Whether you stop the server at the beginning of the HOOK_CHAIN and start at the end, or whether you just do your updates and call for a reload at the end.) Then, once the hook has handled the essential task of preparing to respond to the challenges, it can return to dehydrated, which will handle notifying the ACME server to proceed with challenge verification.

(Edit: whoops, we should be talking about tls-sni-02 challenges, not tls-sni-01!)

@txr13 commented on GitHub (Sep 30, 2016): @j-ed I'm not convinced that pre- and post-deployment hooks are needed. Consider how the DNS challenges are currently deployed. All dehydrated has to do is pass off the domains and list of challenges. The hook script takes care of the details of generating the records and pushing it live, however that needs to happen. For the tls-sni-02 challenges, it would be identical. The deploy_challenge function gets handed a list of domains and challenges (preferably using `HOOK_CHAIN=yes`). The hook script takes care of everything after that--including generating the challenge certificate, updating webserver config, and then calling the webserver's reload or restart function. You don't need any new variables or functions in the hooks at all. From a practical standpoint, the hooking API (list of variables passed, etc.) should be kept identical, and behavior should be kept identical, too. It would be much more effort to essentially make two different hooking APIs (one for DNS, where just challenges are passed, and one for TLS, where you're passing a certificate of some sort). No, I really think that pushing just the domains and challenge info as currently exists is the way to go. Of necessity, that means that the hook script has to handle the challenge certificate generation--but then the hook also has the opportunity to set permissions, certificate location, config updates, and start/stop/restart/reload functions however you please. (Whether you stop the server at the beginning of the HOOK_CHAIN and start at the end, or whether you just do your updates and call for a reload at the end.) Then, once the hook has handled the essential task of preparing to respond to the challenges, it can return to dehydrated, which will handle notifying the ACME server to proceed with challenge verification. (Edit: whoops, we should be talking about tls-sni-_02_ challenges, not tls-sni-_01_!)
Author
Owner

@txr13 commented on GitHub (Sep 30, 2016):

Actually, you know, upon a closer reading of the spec for this... it's not as simple as I thought.

For each SAN on the certificate request, you need a challenge certificate with two SANs (the challenge token and the key auth). And the spec is very clear that these must be the only two SANs on the challenge certificate.

Let's say you wanted an SSL certificate with five domains. That means you need five separate challenge certificates, which each must be served in response to a different SNI. Which means five separate web server configuration blocks, to specify the different server name / certificate combos.

I'm even more inclined to just pass off the list of domains and challenges to the hook, and let the hook deal with that kind of mess. Remembering that Let's Encrypt allows up to 100 names per cetificate... that's an awful lot of challenge certificates and server config blocks to insert / remove. And, this certificate has to be handed out in response to the actual domain name for each validation request--replacing any existing (valid) cert you may still have. So, if you're using this challenge method to renew your cert, users may get an invalid certificate if they visit the site while your certificates are renewing.

@txr13 commented on GitHub (Sep 30, 2016): Actually, you know, upon a closer reading of the spec for this... it's not as simple as I thought. For each SAN on the certificate request, you need a challenge certificate with two SANs (the challenge token and the key auth). And the spec is very clear that these must be the only two SANs on the challenge certificate. Let's say you wanted an SSL certificate with five domains. That means you need five separate challenge certificates, which each must be served in response to a different SNI. Which means five separate web server configuration blocks, to specify the different server name / certificate combos. I'm even more inclined to just pass off the list of domains and challenges to the hook, and let the hook deal with that kind of mess. Remembering that Let's Encrypt allows up to 100 names per cetificate... that's an awful lot of challenge certificates and server config blocks to insert / remove. _And_, this certificate has to be handed out in response to the actual domain name for each validation request--replacing any existing (valid) cert you may still have. So, if you're using this challenge method to renew your cert, users may get an invalid certificate if they visit the site while your certificates are renewing.
Author
Owner

@j-ed commented on GitHub (Sep 30, 2016):

I've also spent some time on this issue and created a small script to get an idea how the certificate could be created, for each challenge. Here is my code snippet based on fixed parameters and the assumption that it is called once for each domain name:

#!/bin/sh
# inspired by: https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md

key_bits='2048'
valid_days='1'
key_file='./tls-sni.key'
cert_file='./tls-sni.crt'
ssl_config='./tls-sni-openssl.cnf'

challenge_token='evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'
key_authorization='e4564532fhwGeg-PRGd-efg0154t4TT-7vveehh12'

# SAN A/B (64 characters)
san_a=`echo "${challenge_token}" | sha256sum | sed 's/[ -]*$//'`
san_a1=`echo "${san_a}" | cut -c1-32`
san_a2=`echo "${san_a}" | cut -c33-64`

san_b=`echo "${key_authorization}" | sha256sum | sed 's/[ -]*$//'`
san_b1=`echo "${san_b}" | cut -c1-32`
san_b2=`echo "${san_b}" | cut -c33-64`

# create dNSName in the following format: x.y.token.acme.invalid
# create dNSName in the following format: x.y.ka.acme.invalid
san_a_subject="${san_a1}.${san_a2}.acme.invalid"
san_b_subject="${san_b1}.${san_b2}.acme.invalid"

# create openssl configuration
{
    echo '[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
C = DE
O = ACME TLS-SNI REQUEST

[v3_req]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:TRUE
subjectAltName = @alt_names

[alt_names]'
    echo "DNS.1 = ${san_a_subject}"
    echo "DNS.2 = ${san_b_subject}"
} > ${ssl_config}

# create openssl certificate
openssl req -x509 -nodes -days ${valid_days} -newkey rsa:${key_bits} -keyout ${key_file} -out ${cert_file} -config ${ssl_config}
@j-ed commented on GitHub (Sep 30, 2016): I've also spent some time on this issue and created a small script to get an idea how the certificate could be created, for each challenge. Here is my code snippet based on fixed parameters and the assumption that it is called once for each domain name: ``` #!/bin/sh # inspired by: https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md key_bits='2048' valid_days='1' key_file='./tls-sni.key' cert_file='./tls-sni.crt' ssl_config='./tls-sni-openssl.cnf' challenge_token='evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' key_authorization='e4564532fhwGeg-PRGd-efg0154t4TT-7vveehh12' # SAN A/B (64 characters) san_a=`echo "${challenge_token}" | sha256sum | sed 's/[ -]*$//'` san_a1=`echo "${san_a}" | cut -c1-32` san_a2=`echo "${san_a}" | cut -c33-64` san_b=`echo "${key_authorization}" | sha256sum | sed 's/[ -]*$//'` san_b1=`echo "${san_b}" | cut -c1-32` san_b2=`echo "${san_b}" | cut -c33-64` # create dNSName in the following format: x.y.token.acme.invalid # create dNSName in the following format: x.y.ka.acme.invalid san_a_subject="${san_a1}.${san_a2}.acme.invalid" san_b_subject="${san_b1}.${san_b2}.acme.invalid" # create openssl configuration { echo '[req] distinguished_name = req_distinguished_name x509_extensions = v3_req prompt = no [req_distinguished_name] C = DE O = ACME TLS-SNI REQUEST [v3_req] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:TRUE subjectAltName = @alt_names [alt_names]' echo "DNS.1 = ${san_a_subject}" echo "DNS.2 = ${san_b_subject}" } > ${ssl_config} # create openssl certificate openssl req -x509 -nodes -days ${valid_days} -newkey rsa:${key_bits} -keyout ${key_file} -out ${cert_file} -config ${ssl_config} ```
Author
Owner

@j-ed commented on GitHub (Mar 28, 2017):

@lukas2511 I've just found the acme.sh project (https://github.com/Neilpang/acme.sh/blob/master/acme.sh) which also uses a shell script attempt to request Let's Encrypt certificates. The author has already implemented TLS-SNI-01 support in his script. It might be worse to have a look on his implementation.

@j-ed commented on GitHub (Mar 28, 2017): @lukas2511 I've just found the acme.sh project (https://github.com/Neilpang/acme.sh/blob/master/acme.sh) which also uses a shell script attempt to request Let's Encrypt certificates. The author has already implemented TLS-SNI-01 support in his script. It might be worse to have a look on his implementation.
Author
Owner

@crapp commented on GitHub (Jan 11, 2018):

The protocol was temporarily disabled because of security concerns

https://community.letsencrypt.org/t/2018-01-09-issue-with-tls-sni-01-and-shared-hosting-infrastructure/49996

@crapp commented on GitHub (Jan 11, 2018): The protocol was temporarily disabled because of security concerns https://community.letsencrypt.org/t/2018-01-09-issue-with-tls-sni-01-and-shared-hosting-infrastructure/49996
Author
Owner

@lbehm commented on GitHub (Jan 12, 2018):

Following this thread https://community.letsencrypt.org/t/tls-sni-via-caa/50172/11 it seems that they are not going to enable TLS-SNI-01 anytime soon. I think we can skip this in dehydrated.

@lbehm commented on GitHub (Jan 12, 2018): Following this thread https://community.letsencrypt.org/t/tls-sni-via-caa/50172/11 it seems that they are not going to enable TLS-SNI-01 anytime soon. I think we can skip this in dehydrated.
Author
Owner

@lukas2511 commented on GitHub (Jan 14, 2018):

Yea it sounds fundamentally flawed so I guess it's not coming back in it's current form. Closing this request for now :)

@lukas2511 commented on GitHub (Jan 14, 2018): Yea it sounds fundamentally flawed so I guess it's not coming back in it's current form. Closing this request for now :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/dehydrated#143