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.