ACME python ClientV2 not able to renew a certificate without enrolling new account?

Hello here!
I have trouble using ACME python lib and to renew a certificate without enrolling another time an account…
I base my code on https://github.com/certbot/certbot/blob/master/acme/examples/http01_example.py

The first time I issue a certificate, no problem:

  • create and save the account key
  • enroll account with .new_account
  • issue certificate with .new_order

But after that, when I need to renew the issued cert, I already have an account enrolled, and so skip .new_account. And then, the .new_order crash:

  File "/root/cozy-coclyco/cozy/coclyco/acme.py", line 275, in _issue_certificate
    order = self.__acme.new_order(pem)
  File "/usr/lib/python3/dist-packages/acme/client.py", line 650, in new_order
    response = self._post(self.directory['newOrder'], order)
  File "/usr/lib/python3/dist-packages/acme/client.py", line 94, in _post
    return self.net.post(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/acme/client.py", line 1130, in post
    return self._post_once(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/acme/client.py", line 1147, in _post_once
    response = self._check_response(response, content_type=content_type)
  File "/usr/lib/python3/dist-packages/acme/client.py", line 999, in _check_response
    raise messages.Error.from_json(jobj)
acme.messages.Error: urn:ietf:params:acme:error:malformed :: The request message was malformed :: No Key ID in JWS header

Looking for this error, I see this issue, allowing querying an account without .new_account before. But I’m lost…

.query_registration take an messages.RegistrationResource as parameter, which is only creatable from .new_account. And even if possible outside this method, Registration seems requiring contact and agreement to be created, which is not available at renew time (only the account key exists on this run).

So, how is it possible to use .new_order on the case of a renew, with no email/registration and only the private account key? Seems certbot achieves this, but looking at the code, I’m not able to understand how it works…

1 Like

I think the issue with the acme module is that it doesn’t provide the equivalent of get_or_create_account - a flow which is supported/encouraged by ACME.

It is almost possible by using messages.NewRegistration(key=jwk.public_key(), only_return_existing=False), but acme.new_account throws a ConflictError if it actually finds an account, leaving you unable to access the regr info.

From my reading, the only way to do it is what Certbot does - serialize and save the messages.RegistrationResource that you get from a successful new_account call, and then pass it in again in ClientNetwork(account=regr, ...) on subsequent visits.

(Edit: here are the places where Certbot seems to save and load the regr, respectively: https://github.com/certbot/certbot/blob/3608abb01a535c35740d82ce37b9ebdef3076886/certbot/account.py#L332-L335, https://github.com/certbot/certbot/blob/3608abb01a535c35740d82ce37b9ebdef3076886/certbot/account.py#L226)

Maybe this is a decent issue to report to Certbot, to make the acme module more usable.

1 Like

Thanks for the response.
This is highly problematic, because porting ACME v1 to v2 seems I break the already enrolled account in v1… I don’t have such .new_account data for such users… :scream:

Well, you only really need the Account URI from the regr, which isn’t too hard to do if you have the private key and it’s a one time thing:

from acme.client import ClientNetwork, ClientV2
from acme import messages
import josepy as jose
import json
import sys


def get_regr_for_account_key(server: str, account_key_path: str) -> messages.RegistrationResource:
    with open(account_key_path, 'r') as f:
        jwk = jose.jwk.JWKRSA.fields_from_json(json.load(f))

    reg = messages.NewRegistration(
        key=jwk.public_key(), only_return_existing=True)

    net = ClientNetwork(jwk)
    directory = messages.Directory.from_json(net.get(server).json())
    client = ClientV2(directory, net)

    response = client._post(directory['newAccount'], reg)
    regr = client._regr_from_response(response)

    return regr


def main():
    regr = get_regr_for_account_key(
        "https://acme-v02.api.letsencrypt.org/directory", sys.argv[1])
    print(f"Account URI: {regr.uri}")


if __name__ == "__main__":
    main()
$ python get_regr_for_key.py ./account.key
Account URI: https://acme-v02.api.letsencrypt.org/acme/acct/68525000

Edit: whoops, sorry, account.key is a serialized JWK, not a PEM. If you have PEMs, you’ll first need to load them into JWKs using josepy.

1 Like

Ok thanks, I will try this :blush: