Failing to register an ACME account

1. The problem I’m having:

I’m testing the ACME implementation in Caddy against Digicert’s ACME service and it’s causing an error - I think in the register phase - and is unable to continue.

2. Error messages and/or full log output:

2024/12/12 16:31:28.247	INFO	tls.obtain	acquiring lock	{"identifier": "test-20241211c.example.ac.uk"}
2024/12/12 16:31:28.247	INFO	tls	finished cleaning storage units
2024/12/12 16:31:28.247	INFO	tls.obtain	lock acquired	{"identifier": "test-20241211c.example.ac.uk"}
2024/12/12 16:31:28.247	INFO	tls.obtain	obtaining certificate	{"identifier": "test-20241211c.example.ac.uk"}
2024/12/12 16:31:28.247	DEBUG	events	event	{"name": "cert_obtaining", "id": "65246b26-6da2-483c-9fa6-3dd005169815", "origin": "tls", "data": {"identifier":"test-20241211c.example.ac.uk"}}
2024/12/12 16:31:28.248	INFO	autosaved config (load with --resume flag)	{"file": "/root/.config/caddy/autosave.json"}
2024/12/12 16:31:28.248	INFO	serving initial configuration
2024/12/12 16:31:28.249	DEBUG	tls.obtain	trying issuer 1/1	{"issuer": "one.digicert.com-mpki-api-v1-acme-v2-directory"}
2024/12/12 16:31:28.555	DEBUG	tls.issuance.acme.acme_client	http request	{"method": "GET", "url": "https://one.digicert.com/mpki/api/v1/acme/v2/directory", "headers": {"User-Agent":["Caddy/2.7.6 CertMagic acmez (linux; arm64)"]}, "response_headers": {"Cache-Control":["no-cache, no-store, max-age=0, must-revalidate"],"Connection":["keep-alive"],"Content-Type":["application/json"],"Date":["Thu, 12 Dec 2024 16:31:28 GMT"],"Expires":["0"],"Pragma":["no-cache"],"Referrer-Policy":["no-referrer"],"Set-Cookie":["visid_incap_2533550=tgHEZfeRTAmMz1OFX8G4hOAPW2cAAAAAQUIPAAAAAABEZIMX4wJqeKYcv7UuAppS; expires=Thu, 11 Dec 2025 23:07:08 GMT; HttpOnly; path=/; Domain=.digicert.com; Secure; SameSite=None","incap_ses_1364_2533550=uBmAAGm1uBmZOKXi4ebtEuAPW2cAAAAAW1W0RnJd0gEdg6kPAx8X/A==; path=/; Domain=.digicert.com; Secure; SameSite=None"],"Strict-Transport-Security":["max-age=15724800"],"Vary":["Accept-Encoding"],"X-Cdn":["Imperva"],"X-Content-Type-Options":["nosniff"],"X-Envoy-Upstream-Service-Time":["3"],"X-Frame-Options":["SAMEORIGIN"],"X-Iinfo":["9-2379172-2379187 NNNY CT(120 124 0) RT(1734021088060 66) q(0 0 0 -1) r(1 1) U2"],"X-Xss-Protection":["0"]}, "status_code": 200}
2024/12/12 16:31:28.717	DEBUG	tls.issuance.acme.acme_client	http request	{"method": "HEAD", "url": "https://one.digicert.com/mpki/api/v1/acme/v2/new-nonce", "headers": {"User-Agent":["Caddy/2.7.6 CertMagic acmez (linux; arm64)"]}, "response_headers": {"Cache-Control":["no-store"],"Connection":["keep-alive"],"Date":["Thu, 12 Dec 2024 16:31:28 GMT"],"Referrer-Policy":["no-referrer"],"Replay-Nonce":["cMNdvyqkg4LdMyygI51oJejwxNiewoSNTYIEDRzcyAsOFXjPmtgRKZpBQndxVV4J"],"Set-Cookie":["visid_incap_2533550=OPxHOksGSVCLquAcYPGD1OAPW2cAAAAAQUIPAAAAAAAkhbbRTI8CY1E/twzY/0EC; expires=Thu, 11 Dec 2025 23:07:08 GMT; HttpOnly; path=/; Domain=.digicert.com; Secure; SameSite=None","incap_ses_1364_2533550=EiG4K4fF5zq8OKXi4ebtEuAPW2cAAAAA5SkNYvNff0irtrzQdYdJ3A==; path=/; Domain=.digicert.com; Secure; SameSite=None"],"Strict-Transport-Security":["max-age=15724800"],"X-Cdn":["Imperva"],"X-Content-Type-Options":["nosniff"],"X-Envoy-Upstream-Service-Time":["6"],"X-Frame-Options":["SAMEORIGIN"],"X-Iinfo":["9-2379172-2379187 SNNy RT(1734021088060 223) q(0 0 0 -1) r(2 2) U6"],"X-Xss-Protection":["0"]}, "status_code": 204}
2024/12/12 16:31:28.921	DEBUG	tls.issuance.acme.acme_client	http request	{"method": "POST", "url": "https://one.digicert.com/mpki/api/v1/acme/v2/new-account", "headers": {"Content-Type":["application/jose+json"],"User-Agent":["Caddy/2.7.6 CertMagic acmez (linux; arm64)"]}, "response_headers": {"Cache-Control":["no-store"],"Connection":["keep-alive"],"Content-Type":["application/problem+json"],"Date":["Thu, 12 Dec 2024 16:31:28 GMT"],"Referrer-Policy":["no-referrer"],"Replay-Nonce":["UKiBkULiLWRPv8ibszuG8mWjeUuCg8VBC-uQCvaaQkP-1K5CbPgtylpQsFVkzIHe"],"Set-Cookie":["visid_incap_2533550=gbE1rgdPQYK3e3e/sMJbZuAPW2cAAAAAQUIPAAAAAAAFr5C9v4+jONYNOzSRJJVO; expires=Thu, 11 Dec 2025 23:07:08 GMT; HttpOnly; path=/; Domain=.digicert.com; Secure; SameSite=None","incap_ses_1364_2533550=1R6mRXrPgHTwOKXi4ebtEuAPW2cAAAAAks7jNlHTVz3YQIIRojT4Cw==; path=/; Domain=.digicert.com; Secure; SameSite=None"],"Strict-Transport-Security":["max-age=15724800"],"X-Cdn":["Imperva"],"X-Content-Type-Options":["nosniff"],"X-Envoy-Upstream-Service-Time":["49"],"X-Frame-Options":["SAMEORIGIN"],"X-Iinfo":["9-2379172-2379187 SNYy RT(1734021088060 383) q(0 0 0 -1) r(2 2) U6"],"X-Xss-Protection":["0"]}, "status_code": 400}
2024/12/12 16:31:28.921	ERROR	tls.obtain	could not get certificate from issuer	{"identifier": "test-20241211c.example.ac.uk", "issuer": "one.digicert.com-mpki-api-v1-acme-v2-directory", "error": "HTTP 0 urn:ietf:params:acme:error:accountrequestError - Unable to process the request., problem \"urn:ietf:params:acme:error:issuance:UnrecognizedPropertyException\": "}
2024/12/12 16:31:28.921	DEBUG	events	event	{"name": "cert_failed", "id": "438165d8-1642-4f8e-ac72-892ab8562ef6", "origin": "tls", "data": {"error":{},"identifier":"test-20241211c.example.ac.uk","issuers":["one.digicert.com-mpki-api-v1-acme-v2-directory"],"renewal":false}}
2024/12/12 16:31:28.921	ERROR	tls.obtain	will retry	{"error": "[test-20241211c.example.ac.uk] Obtain: registering account [mailto:matthew.slowe@jisc.ac.uk] with server: attempt 1: https://one.digicert.com/mpki/api/v1/acme/v2/new-account: HTTP 0 urn:ietf:params:acme:error:accountrequestError - Unable to process the request., problem \"urn:ietf:params:acme:error:issuance:UnrecognizedPropertyException\": ", "attempt": 1, "retrying_in": 60, "elapsed": 0.674111701, "max_duration": 2592000}

3. Caddy version:

v2.8.4

4. How I installed and ran Caddy:

Running Caddy inside Alpine 3.21 OCI container installed from package using apk add caddy

a. System environment:

Docker running Alpine 3.2.1 using Caddy package.

Also tested using the official caddy:alpine and caddy images (which reports as v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=).

b. Command:

caddy run

c. Service/unit/compose file:

N/A

d. My complete Caddy config:

{
	log default {
		level debug
	}
}

test-20241211c.example.ac.uk {
	reverse_proxy http://192.168.37.22:23080 {
		trusted_proxies 0.0.0.0/0 ::/0
	}
	tls {
		issuer acme {
			dir https://one.digicert.com/mpki/api/v1/acme/v2/directory
			eab "x" "y"
			email test@example.ac.uk
			disable_http_challenge
			disable_tlsalpn_challenge
		}
		protocols tls1.2 tls1.3
		ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
	}
}

5. Links to relevant resources:

Unknown

Thanks!

1 Like

I’ve never seen this error before:

\"urn:ietf:params:acme:error:issuance:UnrecognizedPropertyException\": "

I also have never used DigiCert’s ACME endpoints before. Are we sure they are compliant? :thinking:

1 Like

I’ve never seen that error before either, and nor - it seems - has the internet :exploding_head:

I don’t know how to affirm that their endpoints are “compliant” but have tested with various other ACME implementations with no issues (certbot, uacme, acme.sh, and acme-client – all via Alpine if it matters).

I did note that, even with logs turned up to DEBUG, I don’t think it’s recording what was sent in the POST request’s body (that said, I’m not sure how useful it would be either?).

Hm, a quick look at their Certbot docs suggest --register-unsafely-without-email – have you tried without an email address? (You’ll have to clear it from storage, since providing an email is best practice and Caddy will use recent emails.)

1 Like

I am using an email address. This certbot command worked (registered an account and then obtained a certificate, on a fresh Alpine container):

certbot certonly --standalone -d test.example.ac.uk \
    --server https://one.digicert.com/mpki/api/v1/acme/v2/directory \
    --eab-kid x --eab-hmac-key y \
    --register-unsafely-without-email

I did some digging… managed to obtain the payloads for newAccount (the call that’s failing) from Caddy and uacme to compare. Aside from what I presume are expected differences, the Caddy request contains an empty orders field which uacme omits.

The other difference I noted was, within the decoded payload, that Caddy is requesting a P-256 key where uacme is requesting a RSA key. (I did try asking uacme to use a EC key - with -t EC - but it didn’t seem to make a difference to the account registration phase)

I don’t know enough about the ACME protocol to know if orders is allowed to be present in a newAccount request. A quick skim of RFC 8555: Automatic Certificate Management Environment (ACME) (section 7.3) suggests it’s not expected … but, assiming I’m right, I don’t know what a server should do if it sees a value it’s not expecting.

Caddy

{
  "status": "",
  "contact": [
    "mailto:..."
  ],
  "termsOfServiceAgreed": true,
  "externalAccountBinding": {
    "protected": "xxx",
    "payload": "yyy",
    "signature": "zzz"
  },
  "orders": ""
}

uacme

{
  "termsOfServiceAgreed": true,
  "contact": [
    "mailto:..."
  ],
  "externalAccountBinding": {
    "protected": "xxx",
    "payload": "yyy",
    "signature": "zzz"
  }
}
1 Like

I’m just wondering if acmez/acme/account.go:67 (acmez/acme/account.go at master · mholt/acmez · GitHub) should have ,omitempty against it?

diff --git a/acme/account.go b/acme/account.go
index b7a72e0..54ec894 100644
--- a/acme/account.go
+++ b/acme/account.go
@@ -64,7 +64,7 @@ type Account struct {
        // orders (required, string):  A URL from which a list of orders
        // submitted by this account can be fetched via a POST-as-GET
        // request, as described in Section 7.1.2.1.
-       Orders string `json:"orders"`
+       Orders string `json:"orders,omitempty"`

        // In response to new-account, "the server returns this account
        // object in a 201 (Created) response, with the account URL

I may be wildly off though!

1 Like

Right, but I’m saying that the certbot command you’re using doesn’t use an email address: --register-unsafely-without-email

Interesting. Yeah, the spec doesn’t include the orders field in what the client sends when creating the account, but it ALSO says:

The server MUST ignore any values provided in the “orders” fields in
account objects sent by the client, as well as any other fields that
it does not recognize.

So, the DigiCert ACME server is clearly not compliant, whereas Caddy (CertMagic/ACMEz) is not not compliant. :man_shrugging: I guess we could tighten that up a bit (update: pushed a commit), but DigiCert should still fix their ACME server IMO.

The reason I didn’t include ,omitempty in that field is because the ACME spec says the field is required. :man_shrugging:

orders (required, string): A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request, as described in Section 7.1.2.1.

1 Like

Thanks for looking so far. Is there a way I can test this easily? I’ve tried updating Caddy’s go.mod:

diff --git a/go.mod b/go.mod
index 87b0e434..3eceb2c2 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/klauspost/compress v1.17.11
 	github.com/klauspost/cpuid/v2 v2.2.8
-	github.com/mholt/acmez/v2 v2.0.3
+	github.com/mholt/acmez/v3 v3.0.0-20241214053340-45433dfc1161
 	github.com/prometheus/client_golang v1.19.1
 	github.com/quic-go/quic-go v0.48.2
 	github.com/smallstep/certificates v0.26.1

… but it looks like caddyserver/certmagic is also pegged the version at v2.0.3.

Is there some magic way to to this or will I need to fork certmagic and replumb accordingly?

Ta

Ooo, good question, normally the replace directive would be used in go.mod, but I’m not sure how well that works with a major version bump, since the code files explicitly import the /v2 major version. (That’s just how Go modules works.)

You could try replace, let me know how it works for you. Xcaddy can do the same thing: xcaddy build --with github.com/mholt/acmez/v2=>github.com/mholt/acmez/v3@v3.0.0-20241214053340-45433dfc1161 – maybe it’ll work? I dunno.

1 Like

Thanks for the pointers and help so far… no dice though :frowning:

xcaddy thought…

docker run --rm -ti caddy:builder-alpine
xcaddy build --with github.com/mholt/acmez/v2=github.com/mholt/acmez/v3@v3.0.0-20241214053340-45433dfc1161
# ...
go: downloading github.com/mholt/acmez v1.2.0
go: trying upgrade to github.com/mholt/acmez/v3@v3.0.0-20241214053340-45433dfc1161
go: github.com/mholt/acmez/v3@v3.0.0-20241214053340-45433dfc1161 used for two different module paths (github.com/mholt/acmez/v2 and github.com/mholt/acmez/v3)
2024/12/17 09:34:33 [FATAL] exit status 1

Doing it without xcaddy didn’t go much better!

I tried adding this to Caddy’s go.mod:

replace github.com/mholt/acmez/v2 => github.com/mholt/acmez/v3 v3.0.0-20241214053340-45433dfc1161

go build then reports:

/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.21.5-0.20241105180249-4293198e094d/acmeclient.go:29:2: missing go.sum entry for module providing package github.com/mholt/acmez/v2 (imported by github.com/caddyserver/certmagic); to add:
	go get github.com/caddyserver/certmagic@v0.21.5-0.20241105180249-4293198e094d
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.21.5-0.20241105180249-4293198e094d/account.go:35:2: missing go.sum entry for module providing package github.com/mholt/acmez/v2/acme (imported by github.com/caddyserver/certmagic); to add:
	go get github.com/caddyserver/certmagic@v0.21.5-0.20241105180249-4293198e094d

I tried adding the replace to the cached certmagic module and then got lost in a maze of twisty passages all alike and got eaten by a troll :troll:

Is there some way for you to push a branch of certmagic with the updated acmez reference?

It’s about time to tag acmez v3, so that’ll make the module named acmez/v3 at that point, should simplify some things!

2 Likes

Thanks for looking at this!

I’ve done some testing and am still seeing the same error – then I noticed that there was another difference against other ACME implementations, the status field (also empty) is not present in others. I’ve taken the liberty of adding omitempty to that field and rebuilt (successfully, this time).

commit 636b03c8452bf5d7b8fd1b9e7ffcf6b42fa8a2b3 (HEAD -> master, foo/master)
Author: Matthew Slowe <matthew.slowe@jisc.ac.uk>
Date:   Thu Jan 2 12:22:34 2025 +0000

    Omit empty "status" field in Account object

diff --git a/acme/account.go b/acme/account.go
index cdb31a7..2ab2948 100644
--- a/acme/account.go
+++ b/acme/account.go
@@ -38,7 +38,7 @@ type Account struct {
        // initiated deactivation.  See Section 7.1.6.
        //
        // The client need NOT set this field when creating a new account.
-       Status string `json:"status"`
+       Status string `json:"status,omitempty"`

        // contact (optional, array of string):  An array of URLs that the
        // server can use to contact the client for issues related to this

Then rebuilt caddy using this in go.mod:

replace github.com/mholt/acmez/v3 => github.com/jiscfoo/mholt-acmez/v3 v3.0.0-20250102122234-636b03c8452b

Caddy is now able to register an account but the next newOrder request then fails with the same old error:

2025/01/02 12:46:25.225	INFO	tls.issuance.acme	creating new account because no account for configured email is known to us	{"email": "", "ca": "https://one.digicert.com/mpki/api/v1/acme/v2/directory", "error": "open /root/.local/share/caddy/acme/one.digicert.com-mpki-api-v1-acme-v2-directory/users/default/default.json: no such file or directory"}
2025/01/02 12:46:25.225	INFO	tls.issuance.acme	ACME account has empty status; registering account with ACME server	{"contact": [], "location": ""}
2025/01/02 12:46:25.225	INFO	tls.issuance.acme	creating new account because no account for configured email is known to us	{"email": "", "ca": "https://one.digicert.com/mpki/api/v1/acme/v2/directory", "error": "open /root/.local/share/caddy/acme/one.digicert.com-mpki-api-v1-acme-v2-directory/users/default/default.json: no such file or directory"}
2025/01/02 12:46:25.889	INFO	tls.issuance.acme	new ACME account registered	{"contact": [], "status": "valid"}

2025/01/02 12:46:25.894	INFO	tls.issuance.acme	waiting on internal rate limiter	{"identifiers": ["test-20250102.example.ac.uk"], "ca": "https://one.digicert.com/mpki/api/v1/acme/v2/directory", "account": ""}
2025/01/02 12:46:25.894	INFO	tls.issuance.acme	done waiting on internal rate limiter	{"identifiers": ["test-20250102.example.ac.uk"], "ca": "https://one.digicert.com/mpki/api/v1/acme/v2/directory", "account": ""}

2025/01/02 12:46:25.894	INFO	tls.issuance.acme	using ACME account	{"account_id": "https://one.digicert.com/mpki/api/v1/acme/v2/account/ZTEwMmQzODZkN2U3NDBhNWE5MWIxOTA4YmI5MGRmMWM", "account_contact": []}
2025/01/02 12:46:25.894	DEBUG	creating order	{"account": "https://one.digicert.com/mpki/api/v1/acme/v2/account/ZTEwMmQzODZkN2U3NDBhNWE5MWIxOTA4YmI5MGRmMWM", "identifiers": ["test-20250102.example.ac.uk"]}
2025/01/02 12:46:26.101	DEBUG	http request	{"method": "POST", "url": "https://one.digicert.com/mpki/api/v1/acme/v2/new-order", "headers": {"Content-Type":["application/jose+json"],"User-Agent":["Caddy/34cff4af-20241231 CertMagic acmez (linux; arm64)"]}, "response_headers": {"Cache-Control":["no-store"],"Connection":["keep-alive"],"Content-Type":["application/problem+json"],"Date":["Thu, 02 Jan 2025 13:02:03 GMT"],"Referrer-Policy":["no-referrer"],"Replay-Nonce":["DZEpC0u_W-i8pf8oiULbwCxMotQhSiWewmXPQeVHCReEH36On4LFXwB-Lu3fN3EU"],"Set-Cookie":["visid_incap_2533550=7N4NogXnSxmDoDKWPhjktUqOdmcAAAAAQUIPAAAAAACE5xQ7+XjK/v5YSRIVCP4N; expires=Thu, 01 Jan 2026 23:07:29 GMT; HttpOnly; path=/; Domain=.digicert.com; Secure; SameSite=None","incap_ses_1364_2533550=KllxH9z030RdOU7w4ebtEkuOdmcAAAAAew9vnIZOebU8OJysV/RhcA==; path=/; Domain=.digicert.com; Secure; SameSite=None"],"Strict-Transport-Security":["max-age=15724800"],"X-Cdn":["Imperva"],"X-Content-Type-Options":["nosniff"],"X-Envoy-Upstream-Service-Time":["47"],"X-Frame-Options":["SAMEORIGIN"],"X-Iinfo":["4-34425595-34425599 SNYy RT(1735822922647 580) q(0 0 0 0) r(2 2) U6"],"X-Xss-Protection":["0"]}, "status_code": 400}
2025/01/02 12:46:26.102	ERROR	tls.obtain	could not get certificate from issuer	{"identifier": "test-20250102.example.ac.uk", "issuer": "one.digicert.com-mpki-api-v1-acme-v2-directory", "error": "HTTP 400 urn:ietf:params:acme:error:orderrequestError - Unable to process new order request., problem \"urn:ietf:params:acme:error:issuance:UnrecognizedPropertyException\": "}
2025/01/02 12:46:26.102	DEBUG	events	event	{"name": "cert_failed", "id": "bb7ceecf-c915-47c7-817e-435f85a804c2", "origin": "tls", "data": {"error":{},"identifier":"test-20250102.example.ac.uk","issuers":["one.digicert.com-mpki-api-v1-acme-v2-directory"],"renewal":false}}
2025/01/02 12:46:26.102	ERROR	tls.obtain	will retry	{"error": "[test-20250102.example.ac.uk] Obtain: [test-20250102.example.ac.uk] creating new order: attempt 1: https://one.digicert.com/mpki/api/v1/acme/v2/new-order: HTTP 400 urn:ietf:params:acme:error:orderrequestError - Unable to process new order request., problem \"urn:ietf:params:acme:error:issuance:UnrecognizedPropertyException\":  (ca=https://one.digicert.com/mpki/api/v1/acme/v2/directory)", "attempt": 1, "retrying_in": 60, "elapsed": 0.877986214, "max_duration": 2592000}

The payload on this request is:

{
  "expires": "0001-01-01T00:00:00Z",
  "identifiers": [
    {
      "type": "dns",
      "value": "test-20250102.example.ac.uk"
    }
  ],
  "authorizations": null,
  "finalize": "",
  "certificate": ""
}

Clutching at straws, I set authorizations, finalize and certificate to omitempty too:

commit 76e2f1546f97bf695311b7ec8aa0a7aea32efdc8 (HEAD -> master, foo/master)
Author: Matthew Slowe <matthew.slowe@jisc.ac.uk>
Date:   Thu Jan 2 13:10:24 2025 +0000

    Omit "authorizations", "finalize" and "certificate" from Order object if empty

diff --git a/acme/order.go b/acme/order.go
index db20910..68c7df8 100644
--- a/acme/order.go
+++ b/acme/order.go
@@ -78,17 +78,17 @@ type Order struct {
        // required.  For final orders (in the "valid" or "invalid" state),
        // the authorizations that were completed.  Each entry is a URL from
        // which an authorization can be fetched with a POST-as-GET request.
-       Authorizations []string `json:"authorizations"`
+       Authorizations []string `json:"authorizations,omitempty"`

        // finalize (required, string):  A URL that a CSR must be POSTed to once
        // all of the order's authorizations are satisfied to finalize the
        // order.  The result of a successful finalization will be the
        // population of the certificate URL for the order.
-       Finalize string `json:"finalize"`
+       Finalize string `json:"finalize,omitempty"`

        // certificate (optional, string):  A URL for the certificate that has
        // been issued in response to this order.
-       Certificate string `json:"certificate"`
+       Certificate string `json:"certificate,omitempty"`

        // Similar to new-account, the server returns a 201 response with
        // the URL to the order object in the Location header.

… but now appear to be getting badly formed JSON as a payload:

{
  "protected": "...",
  "payload": "eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ",
  "signature": "..."
}

payload decodes to:

{"expires":"0001-01-01T00:00:00Z","identifiers":[{"type":"dns","value":"test-20250102.example.ac.uk"}]

It’s missing the closing }

In delve:

(dlv) stack 0
0  0x0000000000405ab4 in github.com/mholt/acmez/v3/acme.jwsEncodeJSON
   at /root/go/pkg/mod/github.com/jiscfoo/mholt-acmez/v3@v3.0.0-20250102131024-76e2f1546f97/acme/jws.go:100
(truncated)
(dlv) list
> github.com/mholt/acmez/v3/acme.jwsEncodeJSON() /root/go/pkg/mod/github.com/jiscfoo/mholt-acmez/v3@v3.0.0-20250102131024-76e2f1546f97/acme/jws.go:100 (PC: 0x405ab4)
Warning: debugging optimized function
    95:		}
    96:
    97:		var payload string
    98:		if claimset != nil {
    99:			cs, err := json.Marshal(claimset)
=> 100:			if err != nil {
   101:				return nil, err
   102:			}
   103:			payload = base64.RawURLEncoding.EncodeToString(cs)
   104:		}
   105:
(dlv) print string(cs)
"{\"expires\":\"0001-01-01T00:00:00Z\",\"identifiers\":[{\"type\":\"dns\",\"value\":\"test-20250102.example.ac.uk\"}]}"

After base64.RawURLEncoding.EncodeToString() has run, the encoded value is missing the last character:

(dlv) n
> github.com/mholt/acmez/v3/acme.jwsEncodeJSON() /root/go/pkg/mod/github.com/jiscfoo/mholt-acmez/v3@v3.0.0-20250102131024-76e2f1546f97/acme/jws.go:106 (PC: 0x405ae4)
Warning: debugging optimized function
   101:				return nil, err
   102:			}
   103:			payload = base64.RawURLEncoding.EncodeToString(cs)
   104:		}
   105:
=> 106:		payloadToSign := []byte(phead + "." + payload)
   107:		hash := sha.New()
   108:		_, _ = hash.Write(payloadToSign)
   109:		digest := hash.Sum(nil)
   110:
   111:		sig, err := jwsSign(key, sha, digest)
(dlv) print payload
"eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ"

-> base64 -d
{"expires":"0001-01-01T00:00:00Z","identifiers":[{"type":"dns","value":"test-20250102.example.ac.uk"}]

The base64 encoded value appears to be missing two trailing == characters which, incidentally, busybox’s base64 chokes on:

$ echo 'eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ' | base64 -d
{"expires":"0001-01-01T00:00:00Z","identifiers":[{"type":"dns","value":"test-20250102.example.ac.uk"}]base64: truncated input

$ echo 'eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ==' | base64 -d
{"expires":"0001-01-01T00:00:00Z","identifiers":[{"type":"dns","value":"test-20250102.example.ac.uk"}]}

Pulling out the string encoding to a completely separate program:

package main

import (
	"encoding/base64"
	"fmt"
)

func main() {
	data := []byte("{\"expires\":\"0001-01-01T00:00:00Z\",\"identifiers\":[{\"type\":\"dns\",\"value\":\"test-20250102.example.ac.uk\"}]}")
	str := base64.StdEncoding.EncodeToString(data)
	fmt.Println(str)
}
$ go build test.go
$ ./test
eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ==
$ ./test  | base64 -d
{"expires":"0001-01-01T00:00:00Z","identifiers":[{"type":"dns","value":"test-20250102.example.ac.uk"}]}

And I’m out of road for working out where the missing == is going…

So now I have two problems:

  1. Order not working (possibly needing more omitempty settings) causing UnrecognizedPropertyException
  2. Invalid JSON / Base64 strings being generated somewhere
1 Like

Hm, eyJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoidGVzdC0yMDI1MDEwMi5leGFtcGxlLmFjLnVrIn1dfQ decodes properly for me, with the closing }:

I don’t think the payload encoding is a problem.

Maybe I misunderstand, but adding omitempty doesn’t solve the problems though?

Still not convinced this is the case…

1 Like

Apologies I can’t really solve the actual issue, but this little part I can easily explain. There’s nothing wrong here: ACME uses JWS, which uses base64url encoding rather than standard base64. See Appendix C Notes on Implementing base64url Encoding without Padding.

3 Likes

Well there’s an interesting nuance!

I had noticed that the decoding behaviour does seem to vary on how strict the decoder is:

macOS’s is happy (but omits the closing }) while busybox’s emits a warning. C’est la vie!

I’ll keep digging… do you have any thoughts on the omitempty changes in Order? I was getting a little lost in the RFC weeds.

1 Like

I perhaps got hung up on the (apparent) base64 encoding thing and will keep digging

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.