Account Deactivation with ‘kid’ Field per ACME 8555

When I use https://acme-staging-v02.api.letsencrypt.org/directory to test my account deactivate code in my personal ACME client using ‘kid’ and not ‘jwk’ in the protected header of the JWS, I get this problem document:
{'type': 'urn:ietf:params:acme:error:malformed', 'detail': 'No embedded JWK in JWS header', 'status': 400}

I appreciate the clarity of the error message. As you likely suppose, the correct ‘jwk’ was just given to the LE ACME test server to get the ‘kid’ for the account deactivation request that was denied as shown above.

My understanding of the ACME2 protocol is that one but not both of the ‘kid’ and ‘jwk’ fields is acceptable (and required) for all services except for account key rollover. I realize that the account key rollover request (RFC 8555, part 7.3.5) requires the jwk of the current account key to be decommissioned and, not surprisingly, requires the jwk of the proposed replacement account key.

Could it be that the requirement for the (current) account key's public jwk for the account rollover service and the lack of such a requirement (with what I can find) are inconsistent? I can guess that if the public account key JWK is somehow necessary for account key rollover (in the payload of the inner JWS not the header), then that same requirement would naturally apply to the ‘rollover with no replacement account key’ called account deactivation.

If the ‘kid’ is acceptable for account deactivation according to the standard to be implemented, then consider this commentary to be a feature request. If it is not acceptable, please refer me to the documentation that makes it so or consider this commentary to be a documentation request.

I end this commentary by showing what I found to come to my understanding that the ‘kid’ is acceptable for account deactivation per the standard.

RFC 8555, part 7.3.6 shows an example deactivation request message with a summary exposition:
POST /acme/acct/evOfKhNU60wg HTTP/1.1
Host: example.com
Content-Type: application/jose+json

{
  "protected": base64url({
    "alg": "ES256",
    "kid": "https://example.com/acme/acct/evOfKhNU60wg",
    "nonce": "ntuJWWSic4WVNSqeUmshgg",
    "url": "https://example.com/acme/acct/evOfKhNU60wg"
  }),
  "payload": base64url({
    "status": "deactivated"
  }),
  "signature": "earzVLd3m5M4xJzR...bVTqn7R08AKOVf3Y"
}

The server MUST verify that the request is signed by the account key.  If the server accepts the deactivation request, it replies with a 200 (OK) status code and the current contents of the account object.

The example uses the “kid|” field. The summary exposition says that the JWS must be signed by the account key. That is no indication that the ‘kid’ is unacceptable for this request.

Now let's look at the very start of the same part 7.3.6.

RFC 8555, part 7.3.6:
A client can deactivate an account by posting a signed update to the account URL with a status field of "deactivated". […]

The phrase ‘signed update’ is relevant if we want to squeeze out as much information as we can. That phrase make implicit reference to RFC 8555, part 7.3.2, which has the part title “Account Update”. The example of that part used the “kid” field not the “jwk” field.

What is the standard regarding the use of ‘jwk’ and ‘kid’ for account key rollover and account (key) deactivation? What am I supposed to code for my client? My design before this issue was discovered was to always use the ‘kid’ if I have it and if I am not requesting an account rollover. I always have the ‘kid’ after I retrieve the directory of service URLs from the ACME server.

1 Like

Supplying a kid in the JWS during account deactivation is correct (and the only correct thing). Can you share an example JWS you're sending that doesn't validate? What URL are you POSTing to?

4 Likes

Nummer378, these are my invocation (yes, it's camelCase) and log with (probably) all the details.

deactivate-invocation-20250330.txt (1.1 KB)
deactivate-log-20250330.txt (6.7 KB)

One more thing: if I change the code to use the account key's JWK, it works as expected.

Entered function make_jws() with protected_obj {'url': 'https://acme-staging-v02.api.letsencrypt.org/acme/new-acct', 'alg': 'ES384', 'kid': 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/190414184', 'nonce': 'rEioG24ducpotyB229nAzHfqfGOZsooOf2E42ZMfE4d7pba0VuE'} and payload_obj {'status': 'deactivated'} .
Using 'alg' ES384 for proposed JWS with key type P-384.
Attempting to sign with key of type P-384 at test-account-key.pem with command list ['openssl', 'pkeyutl', '-sign', '-inkey', 'test-account-key.pem', '-rawin', '-digest', 'sha384', '-asn1parse'].
The signature made with a P-384 key is 96 octets long.

In query_acme_server() send_body_dict is:

{'protected': 'eyJ1cmwiOiAiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctYWNjdCIsICJhbGciOiAiRVMzODQiLCAia2lkIjogImh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC8xOTA0MTQxODQiLCAibm9uY2UiOiAickVpb0cyNGR1Y3BvdHlCMjI5bkF6SGZxZkdPWnNvb09mMkU0MlpNZkU0ZDdwYmEwVnVFIn0', 'payload': 'eyJzdGF0dXMiOiAiZGVhY3RpdmF0ZWQifQ', 'signature': 'lsJ_GQAaeLNJZG2oFNu9Va5EwoLvUS668aY7Ioq3AuceaVhdkIcdo82qXSpHfXmMNkXW8KVuisLzAPkmbz5AdhRZVMLvEKGs5ZZjJwS69obuXhFhsPL4bz98HT4SR59t'} .

You are posting the account deactivation request to the newAccount directory URL https://acme-staging-v02.api.letsencrypt.org/acme/new-acct. This is not correct, you need to post to the account URL itself, see RFC8555:

A client can deactivate an account by posting a signed update to the account URL [...]

The example you referenced earlier also shows that the URL is the account's URL returned by ACME server during account registration.

6 Likes

Thank you, Nummer378. You are exactly right on my code for account deactivation. It works as expected with that simple change from "directory_dict['newAccount']" to "kid".

I don't understand what you mean about 'the example I referenced earlier'. In my only previous thread-starter post, "Misleading Catchall", I cite several examples that show the catchall behavior, and as I understand the code excerpt given by orangepizza, the use of 'Parse error reading JWS' is as I said, a misleading catchall.

The 'standard' of algorithms supported by the server is presumed for the client, and the return value of variable 'err' in the boulder code excerpt is never used, as I read the code excerpt. Also, my account rollover code goes to the correct target URL: "directory_dict['keyChange']". I never showed an invocation or log for that example. I did show an example of another client developer who expected 'something like "bad public key"’.

I don't know how to negotiate with the server for a supported key type or for a supported RSxxx algorithm without hacking assumptions which would result in my code presuming incorrectly in some use cases that a key or signature algorithm change would fix the failed attempt to get the service from the server when in fact something else is wrong.

I don't know if this has anything to do with your second point, but I would like to automatically use the strongest algorithms supported by my client and the server, and let the servers upgrade their algorithms whenever they do. Then my client code, if not behind the times in what it supports, would just keep up with the cryptology practices of ACME servers.

As it is now, if LE ACME servers were to add 'RS512' or 'RS384' and to drop 'RS256', my code would fail without manual intervention. The fact that it would be a simple code change does not change the fact that the situation is a barrier to automatic certification. If I were to try another ACME server that had support for RSA keys but did not support 'RS256', my code would fail the same way. I want a hands free solution. My client code supports (bugs or not) Edwards curve keys because OpenSSL does and I added it. Of course, I could be making a mistake in my interpretation of information and going completely in the wrong conceptual direction.

RFC 8555 provides for a list of supported signing algorithms in a certain situation, as I explained in the previous thread I started. I might like to see a directory entry for a data structure that shows exactly what key types and signing algorithms the server currently supports with disambiguation for accepted account and certification keys. If that info were provided on a sufficiently specific failure message, I suppose that would be okay-ish. I leave protocol design to the experts, but I have my opinions. A good way choose key types and/or signing algorithms could be available and I simple don't recognize it.

Just my two cents. I am an amateur programmer. I don't understand lots of things.

I think I can finish developing a decent client (to my liking) for automated renewals that can be updated almost easily o handle new key types and signing algorithms as they are implemented by OpenSSL. I am not far. Just need to chain the end-entity certs and I'll be in pretty good shape.

Thanks, Nummer378, for your kind help and consideration. Thanks, orangepizza and everybody. It's nice to be able to get my certs automatically and the way I want it. This is a cool technology.

1 Like

I had this one in mind, the RFC 8555 example

6 Likes

Got it. I just looked at the example in RFC 8555, part 7.3.6 again. Apparently, I had not realized that the POST target URL (or whatever it should be called) is the exact same URL as the value of the 'kid' field in the protected header. I see it now.

4 Likes

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