Dns-01 challenge using lets encrypts' staging-server with acme-client and digitalocean as provider


#1

My domain is:

I am using:

  • acme-client by Kristapsdz

I ran this command:

  • sh /etc/periodic/weekly/acme_client -vv

It produced this output (:

acme-client: /etc/acme/manual.uzoagu.com/privkey.pem: account key exists (not creating)
acme-client: /etc/ssl/acme/private/manual.uzoagu.com/privkey.pem: domain key exists (not creating)
acme-client: https://acme-staging.api.letsencrypt.org/directory: directories
acme-client: acme-staging.api.letsencrypt.org: DNS: 104.107.50.145
acme-client: acme-staging.api.letsencrypt.org: DNS: 2600:1400:d:18a::3a8e
acme-client: acme-staging.api.letsencrypt.org: DNS: 2600:1400:d:18b::3a8e
acme-client: https://acme-staging.api.letsencrypt.org/acme/new-authz: req-auth: manual.uzoagu.com
dns-01 manual.uzoagu.com RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss.r38k7OetWSqV1aGRgxUKqVtKtoC1ppdRO5VQr3G_p7c
dns-01 manual.uzoagu.com RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss.r38k7OetWSqV1aGRgxUKqVtKtoC1ppdRO5VQr3G_p7c
acme-client: https://acme-staging.api.letsencrypt.org/acme/challenge/8nDkoajsImD1eqi2t71y4IoIWDTExWBodoQ3yA8x8eQ/158629609: challenge
acme-client: https://acme-staging.api.letsencrypt.org/acme/challenge/8nDkoajsImD1eqi2t71y4IoIWDTExWBodoQ3yA8x8eQ/158629609: status
acme-client: https://acme-staging.api.letsencrypt.org/acme/challenge/8nDkoajsImD1eqi2t71y4IoIWDTExWBodoQ3yA8x8eQ/158629609: bad response
acme-client: transfer buffer: [{ "type": "dns-01", "status": "invalid", "error": { "type": "urn:acme:error:unauthorized", "detail": "Incorrect TXT record \"RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss\" found at _acme-challenge.manual.uzoagu.com", "status": 403 }, "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/8nDkoajsImD1eqi2t71y4IoIWDTExWBodoQ3yA8x8eQ/158629609", "token": "RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss", "keyAuthorization": "RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss.r38k7OetWSqV1aGRgxUKqVtKtoC1ppdRO5VQr3G_p7c" }] (546 bytes)

My web server is (include version):
nginx

The operating system my web server runs on is (include version):
ubuntu 18.04

My hosting provider, if applicable, is:
digital ocean

I can login to a root shell on my machine (yes or no, or I don’t know):
Yes

So this isn’t an issue per say; its more of not understanding how the response the client returns maps to TXT records (given that each DNS api has a slightly different implementation).

Reading through the README, I see this:

When using -t, each domain (primary and altnames) is authorised over standard output 
and input between the caller and acme-client as follows:

  (a). acme-client prints “challenge-type dns-domain token.thumbprint\n” (note the trailing newline) on 
        its standard output.

  (b). The caller performs any tasks to implement the challenge's response.
  
  (c). The caller writes the same three-field string and the newline to the standard input of acme-client.

This cycle repeats for each requested domain, then acme-client exits.

My question is:

  • Which of the 3 part string in (a) response corresponds to TXT VALUE ?

I have tried using:

  • all 3 parts combined (respecting the spaces between the 1st two and the period that joins the token
    and thumbprint)
  • only the token
  • only the thumbprint

None of the above work; instead returning errors that match this:

acme-client: transfer buffer: [
  { 
    "type": "dns-01", 
    "status": "invalid", 
    "error": { 
      "type": "urn:acme:error:unauthorized", 
      "detail": "Incorrect TXT record \"TChR2DfPtEOyWaxl750J4E_sJo97szwCVHq3PT5NfRU.LyF9F8lc51hP9u3aOG7Lwnt-3DnMV2MpLi0RgHGM-VA\" found at _acme-challenge.sub.domain.com", 
      "status": 403 
    }, 
  "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/LPRoT8sfbwpbQU99UXs10jI1VSn9HyhfIFDcvcDlo9Y/158596424", 
  "token": "2Q_pQKPWiun16FT60BGriRh1Tcb7fXrmOCOLOYXXTPc", 
  "keyAuthorization": "2Q_pQKPWiun16FT60BGriRh1Tcb7fXrmOCOLOYXXTPc.LyF9F8lc51hP9u3aOG7Lwnt-3DnMV2MpLi0RgHGM-VA" 
  }
] 
(590 bytes)
acme-client: bad exit: netproc(48): 1

I also ran dig -t txt _acme-challenge.sub.domain.com and confirmed got this results:

; <<>> DiG 9.10.6 <<>> -t txt _acme-challenge.some.subdomain.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1043
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;_acme-challenge.some.subdomain.com. IN  TXT

;; ANSWER SECTION:
_acme-challenge.some.subdomain.com. 3600 IN TXT  "RogJGpjq1B6_3EyvUzkBhWMS7SwodiInW3bwgsYPpss"

;; Query time: 48 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Sat Aug 11 15:29:18 EDT 2018
;; MSG SIZE  rcvd: 118

I’m using a control panel to manage my site (no, or provide the name and version of the control panel):


#2

Hi @pnotes

all three are wrong.

There must be a string: Token, dot and the JWK-Footprint of the key (keyAuthorization).

Then the SHA256-Hash of this value, converted to Base64. Then the transformation Base64 -> special-base64.

Your first invalid-result: There you used the token.
The second invalid: There is the keyAuthorization used.

The specification:

https://tools.ietf.org/html/draft-ietf-acme-acme-13#section-8.4

A client fulfills this challenge by constructing a key authorization from the “token” value provided in the challenge and the client’s account key. The client then computes the SHA-256 digest [FIPS180-4] of the key authorization. The record provisioned to the DNS contains the base64url encoding of this digest.


#3

hi @JuergenAuer!
Thanks for the response; the explanation and the link to the specification was very helpful.

Unfortunately, I am still getting an error. I performed the actions to transform the token.KeyAuthorization into a base64 value by running the following:

echo -n "ELosIinUS27415zd12jSfwGa9OAzEQLtgfYkpcm6c1M.r38k7OetWSqV1aGRgxUKqVtKtoC1ppdRO5VQr3G_p7c" | openssl dgst -sha256 | base64

the output of that was placed in the value field TXT records.

Unfortunately, that returned this:

[
  { 
    "type": "dns-01", 
    "status": "invalid", 
    "error": { 
      "type": "urn:acme:error:unauthorized", 
      "detail": "Incorrect TXT record \"KHN0ZGluKT0gODJjNDQ3NjdjMmQzNzdkM2ViZmExNjMyZmU2ZjljOTk0NTk1ZTgyNDgwYzk2OGYyMjYyZTM1YjIyODZmZDY1Zgo=\" found at _acme-challenge.manual.uzoagu.com", 
      "status": 403 
    }, 
  "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/gwuWSXpwmZjEzg4GH0PLcZX02CIOkRJ6SxjN4kLW764/158645520", 
  "token": "ELosIinUS27415zd12jSfwGa9OAzEQLtgfYkpcm6c1M", 
  "keyAuthorization": "ELosIinUS27415zd12jSfwGa9OAzEQLtgfYkpcm6c1M.r38k7OetWSqV1aGRgxUKqVtKtoC1ppdRO5VQr3G_p7c" 
  }
]
(603 bytes)
acme-client: bad exit: netproc(8): 1

Question:
You wrote this: _ Then the transformation Base64 -> special-base64._ I don’t know what that last step (transform Base64 value into a special-base64?) refers to. Can you please clarify? It would be greatly appreciated; thank you.


#4

The standard-base64 - Encoding may have + and / and may end with one or two =.

These characters may produce problems. So there is a base64url-encoding.

Input s (Pseudocode):

– Remove = at the end
s = s.split(’=’)[0]
– Replace + --> -
s = s.Replace(’+’, ‘-’)
– Replace / --> _
s = s.Replace(’/’, ‘_’)
Return s

Your SHA256 has a ‘=’ at the end, this is not base64url encoded.


#5

@JuergenAuer Ha! Got it. Thanks. Will try now. Thank you so much for helping and explaining.


#6

@JuergenAuer, so giving you an update (not sure that you care though).

Things are still now working. I have decided to step away from this issue for now because I am getting frustrated (and that never helps :grinning:).

2 thoughts that occurred to me while working through this:

  • this link mentions:
When the identifier being validated is a domain name, the client can prove control of that domain by provisioning a TXT resource record containing a designated value for a specific validation domain name.

type (required, string): : The string "dns-01"

token (required, string): : A random value that uniquely identifies the challenge. This value MUST have at least 128 bits of entropy. It MUST NOT contain any characters outside the base64url alphabet, including padding characters ("=").

GET /acme/authz/1234/2 HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "type": "dns-01",
  "url": "https://example.com/acme/authz/1234/2",
  "status": "pending",
  "token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
}
**A client fulfills this challenge by constructing a key authorization from the "token" value provided in the challenge and the client's account key**.

the last sentence stands out because I was performing a transformation using the value of token and the keyAuthorization returned in the acme-client response. The readme of acme-client just refers to this as a thumbprint. I know that a thumbprint is something used in authorization/authentication BUT in this content its value/function is unclear (to me at least) - thus folks like me resort to trial and error and run into rate limits. I couldn’t try out signing with the client key (which apparently live is /etc/acme/domain-name/privkey.pem) because of rate issues but will do so soon as I can.

The 2nd question that occurred to me had to do with the effect of using a staging server:
Basically, when I get an error using the staging-server, I have no idea if the error is due to that fact that the TXT value isn’t valid because it is issued by the staging server or if it is invalid because the TXT value is actually wrong. .

Anyway, I am just thinking out loud now before I hit the sack.

Thanks for reading.


#7

The Key Authorization uses the JWK_Thumbprint:

https://tools.ietf.org/html/draft-ietf-acme-acme-13#section-8.1

The “JWK_Thumbprint” step indicates the computation specified in [RFC7638], using the SHA-256 digest [FIPS180-4].

The Example from RFC7638:

https://tools.ietf.org/html/rfc7638#section-3.1

The JWK_Thumbprint is a JSON-Object of the public key with a defined order of the names and without white spaces. Then the UTF-8 representation, then the octets -> SHA256 + base64url.

If you create a new order, there is a order-url. Something like

https://acme-staging-v02.api.letsencrypt.org/acme/order/yourAccountId/yourOrderId

Open this in a browser. There are authorizations, one per domain name. Follow these links, there you see the result: pending / valid / invalid. If invalid, the reason.


#8

Good morning @JuergenAuer,

Again, thanks for the explanations and links.

Reading through the README, I see this:

When using -t, each domain (primary and altnames) is authorised over standard output 
and input between the caller and acme-client as follows:

  (a). acme-client prints “challenge-type dns-domain token.thumbprint\n” (note the trailing newline) on 
        its standard output.

  (b). The caller performs any tasks to implement the challenge's response.
  
  (c). The caller writes the same three-field string and the newline to the standard input of acme-client.

This cycle repeats for each requested domain, then acme-client exits.

I am reading through sections 8.1 - 8.4 of the ietf spec for acme.

  • spec says:

A client fulfills this challenge by constructing a key authorization
from the “token” value provided in the challenge and the client’s
account key.

acme-client json response contains a keyAuthorization field:

  acme-client: transfer buffer: [
    ...
    "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/LPRoT8sfbwpbQU99UXs10jI1VSn9HyhfIFDcvcDlo9Y/158596424", 
    "token": "2Q_pQKPWiun16FT60BGriRh1Tcb7fXrmOCOLOYXXTPc", 
    "keyAuthorization": "2Q_pQKPWiun16FT60BGriRh1Tcb7fXrmOCOLOYXXTPc.LyF9F8lc51hP9u3aOG7Lwnt-3DnMV2MpLi0RgHGM-VA" 
    }
  ] 

Spec goes on to say:

The client then computes the SHA-256 digest [FIPS180-4] of the key authorization. The record provisioned to the DNS contains the base64url encoding of this digest.

So I do this:

echo mKJ0LZl4leYPbQpTVDGySfbMJygw8cCJZkiAP9r-9KY.cB_s97CUBrQMLZTLbiy4U1Mzi2nwBM7ZL6PdELcX1Ls | openssl dgst -sha256 | base64 | tr '+\/' '-_' | tr -d '='

and place the return value of into the TXT field of my provider.

The above steps should cover the 2. The caller performs any tasks to implement the challenge’s response.

Then I write to the stand input of acme-client as instructed in 3. The caller writes the same three-field string and the newline to the standard input of acme-client.

And yet, each time the result is a 403 error that looks like this:

{
  "type": "dns-01",
  "status": "invalid",
  "error": {
    "type": "urn:acme:error:unauthorized",
    "detail": "Incorrect TXT record \"KHN0ZGluKT0gYmFmOGVkMmIwY2UxZjM1NDk0ODJiZTQ1ZjBlOGQyYmQ0NzgxOTk3YTM2N2FhNjFkNGQzYjUzMjQ4ODY5MDVmZQo\" found at _acme-challenge.manual.uzoagu.com",
    "status": 403
  },
  "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/zfL7sjh4R0l3UjW-5HuZRDjA0OcbMgIGwEkDd9t1ML0/158926539",
  "token": "mKJ0LZl4leYPbQpTVDGySfbMJygw8cCJZkiAP9r-9KY",
  "keyAuthorization": "mKJ0LZl4leYPbQpTVDGySfbMJygw8cCJZkiAP9r-9KY.cB_s97CUBrQMLZTLbiy4U1Mzi2nwBM7ZL6PdELcX1Ls"
}

At this point, I want to know if the error lies on my end or somewhere else. What am I missing? I feel like I have read that (very very DRY SPEC) at least 3 times now from top to bottom. Sigh


#9

I am not so firm using openssl.

But my own code must be correct, because I have created wildcard certificates with dns-01 - challenge with this code via Letsencrypt.

Testing with your input (NET-Code):

_s = “mKJ0LZl4leYPbQpTVDGySfbMJygw8cCJZkiAP9r-9KY.cB_s97CUBrQMLZTLbiy4U1Mzi2nwBM7ZL6PdELcX1Ls”
_result = Base64url(Convert.ToBase64String(_sha256.ComputeHash(Encoding.UTF8.GetBytes(_s))))
_result = “xq5lEWHory-ilaMreevDuCozYloIz365dzSI1OVfBao”
Length: 43 character

One own correct result: c6uy2X8HPR4QmJ5yYA5EzTo8rlRqVlMSiGuCaVl7cLM - also 43 char (last wildcard certificate).

Your result:

KHN0ZGluKT0gYmFmOGVkMmIwY2UxZjM1NDk0ODJiZTQ1ZjBlOGQyYmQ0NzgxOTk3YTM2N2FhNjFkNGQzYjUzMjQ4ODY5MDVmZQo
Length: 99 character

A SHA256-output has 32 byte. But your function may create the output as Hex-Code, so 32 byte -> 64 char. Base64 is something like * 1,33 = 85 char. Looks like your string is too long.


#10

Thanks to your code snippet, I figured it out; had to output the digest in binary form before base64 encoding it.

@JuergenAuer you are a GEM. Thank you so so much - learnt a lot from you (esp. to read the specs ALWAYS).