Skip to content

(Advanced) Manual HTTP Challenge Validation

Ryan Bolger edited this page Sep 1, 2021 · 9 revisions

NOTE: This content is out of date. Posh-ACME 4.x and newer supports plugins for HTTP validation.

(Advanced) Manual HTTP Challenge Validation

Intro

The beauty of the ACME protocol is that it's an open standard. And while Posh-ACME primarily targets users who want to avoid understanding all of the protocol complexity, it also exposes functions that allow you to do things a bit closer to the protocol level than just running New-PACertificate and Submit-Renewal. This can enable more advanced automation scenarios and allow you to support additional challenge types that the module doesn't directly support yet. This tutorial will focus on the latter and walk through how to generate a certificate using the http-01 challenge.

From a high level, the ACME conversation looks more or less like this:

  • Create an account
  • Create a certificate order
  • Prove control of the "identifiers" (names) in the requested cert by answering challenges
  • Finalize the order by submitting a CSR
  • Download the signed certificate

If you're curious about what's going on under the hood during this tutorial, it is advised to append -Verbose to your commands or run $VerbosePreference = 'Continue'. If you really want to get deep, you can also turn on debug logging by running $DebugPreference = 'Continue'. The defaults for both of those preferences are SilentlyContinue if you want to change them back later.

Server Selection

It is always advised not to use the production Let's Encrypt server while testing code. The staging server is the easiest alternative, but still has some rate limits that you can run afoul of if you're not careful. There is also Pebble which is a tiny ACME server you can self-host and is built for testing code against. For simplicity, we'll select the Let's Encrypt staging server.

Set-PAServer LE_STAGE

Account Setup

Requesting a certificate always starts with creating an account on the ACME server which is basically just a public/private key pair that is used to sign the protocol messages you send to the server along with some metadata like one or more email addresses to send expiration notifications to. If you've been previously using the module against the staging server, you likely already have an account. If so, you can either skip this section or create a second account which is also supported.

New-PAAccount -AcceptTOS -Contact 'me@example.com'

Create an Order

The only required parameter for a new order is the set of names you want included in the certificate. Optional parameters include things like -KeyLength to change the private key type/size, -Install which tells Posh-ACME to automatically store the signed cert in the Windows certificate store (requires local admin), and -PfxPass which lets you set the decryption password for the certificate PFX file.

In this example, we're not going to create a wildcard cert because they require using DNS challenge validation and we're going to be dealing with HTTP challenges.

$domains = 'site1.example.com','site2.example.com'
New-PAOrder $domains

Assuming you didn't use names that were previously validated on this account, you should get output that looks something like this where the status is pending. If the status is ready, create an order with different names that haven't been previously validated.

MainDomain        status  KeyLength SANs              OCSPMustStaple CertExpires
----------        ------  --------- ----              -------------- -----------
site1.example.com pending 2048      site2.example.com False

Authorizations and Challenges

The distinction between an order, authorization, and challenge can be confusing if you're not familiar with the ACME protocol. So let's clarify first. An order is a request for a certificate that contains one or more "identifiers", otherwise known as names like site1.example.com. Each identifier in an order has an authorization object associated with it that indicates whether this account is authorized to get a cert for that name. New authorizations start in a pending state awaiting the client to complete a challenge associated with that authorization. Each authorization can have multiple different challenges (DNS, HTTP, etc) that indicate the different methods the ACME server will accept to prove ownership of the name. You only need to complete one of the offered challenges in order to satisfy an authorization.

Get-PAAuthorizations can be used with the output of Get-PAOrder to retrieve the current set of authorizations (and their challenges) for an order. So lets put those details into a variable and display them.

$auths = Get-PAOrder | Get-PAAuthorizations
$auths

This should give an output that looks something like this. The first status column is the overall status of the authorization. The last two columns are the status of the dns-01 and http-01 challenges. Normally the challenge specific details are buried a bit deeper in the challenges property, but Posh-ACME tries to help by pulling out the important bits into properties on the root object.

fqdn              status  Expires              DNS01Status HTTP01Status
----              ------  -------              ----------- ------------
site1.example.com pending 8/13/2018 9:52:23 AM pending     pending
site2.example.com pending 8/13/2018 9:52:23 AM pending     pending

Let's take a look at the full details of one of the authorization objects by running $auths[0] | fl. You should get an output like this:

identifier   : @{type=dns; value=site1.example.com}
status       : pending
expires      : 2018-08-13T16:52:23Z
challenges   : {@{type=dns-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<DNS_CHAL_ID>; token=<DNS_TOKEN>},
               @{type=http-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<HTTP_CHAL_ID>; token=<HTTP_TOKEN>},
               @{type=tls-alpn-01; status=pending; url=https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<ALPN_CHAL_ID>; token=<ALPN_TOKEN>}}
DNSId        : site1.example.com
fqdn         : site1.example.com
location     : https://acme-staging-v02.api.letsencrypt.org/acme/authz/<AUTH_ID>
DNS01Status  : pending
DNS01Url     : https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<DNS_CHAL_ID>
DNS01Token   : <DNS_TOKEN>
HTTP01Status : pending
HTTP01Url    : https://acme-staging-v02.api.letsencrypt.org/acme/challenge/<AUTH_ID>/<HTTP_CHAL_ID>
HTTP01Token  : <HTTP_TOKEN>

The things we care about in this example are the HTTP01Url and HTTP01Token properties. The token value is what we're going to use to prove we control this identifier. The URL is how we inform the ACME server that it should perform the validation check for the challenge.

Publishing an HTTP Challenge

There are two things you need to satisfy an HTTP challenge, the URL being requested and the HTTP response body the ACME server expects. The URL is comprised of the identifier's FQDN and the HTTP token value like this.

http://<FQDN>/.well-known/acme-challenge/<TOKEN>

The response body is a "key authorization" value which is comprised of the token value and the account's public key thumbprint. The Get-KeyAuthorization function can be used to generate this value. So putting everything together, you might do something like this to build your publishing details.

$toPublish = $auths | Select @{L='Url';E={"http://$($_.fqdn)/.well-known/acme-challenge/$($_.HTTP01Token)"}}, `
                             @{L='Body';E={Get-KeyAuthorization $_.HTTP01Token (Get-PAAccount)}}

Now it's up to you to actually make those publishing details available on your web server. From the Internet, you should be able to go to those URLs and receive a response with those body strings. Once you can do that, proceed to the next step.

Note: If you're using PowerShell functions to create the challenge files, make sure to specify ASCII/ANSI file encoding. Line endings shouldn't matter, but the encoding does in my testing. So if you're using Out-File, add -Encoding ascii.

Notify the ACME Server

This step simply asks the ACME server to do its own check against the challenges you just published. Use the Send-ChallengeAck function like this.

$auths.HTTP01Url | Send-ChallengeAck

The challenges are usually validated pretty quick. But there may be a delay if the ACME server is overloaded. You can poll the status of your authorizations by re-running Get-PAOrder | Get-PAAuthorizations. Eventually, the status for each one will either be "valid" or "invalid". Good output should look something like this.

fqdn              status Expires              DNS01Status HTTP01Status
----              ------ -------              ----------- ------------
site1.example.com valid  9/6/2018 12:39:36 AM pending     valid
site2.example.com valid  9/6/2018 12:39:36 AM pending     valid

Finishing Up

Now that you have all of your identifiers authorized, your order status should now be "ready" which you can check with Get-PAOrder -Refresh. It should look something like this.

MainDomain        status KeyLength SANs              OCSPMustStaple CertExpires
----------        ------ --------- ----              -------------- -----------
site1.example.com ready  2048      site2.example.com False

The easiest thing to do now is actually to use New-PACertificate with the $domains variable that you created at the beginning. It's smart enough to pick up your in-progress order and finish it up for you.

New-PACertificate $domains

Use Get-PACertificate | fl to get a full list of cert properties including the filesystem paths where the files are stored.

Debugging Challenge Failures

If for some reason one or more of your challenge validations failed, you can retrieve the error details from the ACME server like this.

(Get-PAOrder | Get-PAAuthorizations).challenges.error | fl

Renewals

The concept of a renewal doesn't actually exist in the ACME protocol. What most clients call a renewal is just a new order with the same parameters as last time. So the only thing extra you need to deal with is knowing when to renew. When you successfully complete a certificate order, Posh-ACME will attach a RenewAfter property to the order object which you can use to calculate whether it's time to renew or not. The property is an ISO 8601 date/time string which can be parsed and checked with DateTimeOffset like this.

$renewAfter = [DateTimeOffset]::Parse((Get-PAOrder).RenewAfter)
if ([DateTimeOffset]::Now -gt $renewAfter) {
    # time to renew
}