Skip to content

Gufo ACME Examples: Register ACME Account

We have mastered how to create a Certificate Signing Request in our get_csr example. This guide will drive you through an ACME account registration process. An account registration process is crucial to perform all other operations, ACME is intended to: The certificate signing.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))

The code is straightforward:

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))

AcmeClient is an asynchronous client, so we need asyncio.run() function to launch it.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))

Import sys module to parse the CLI argument.

Warning

We use sys.argv only for demonstration purposes. Use argsparse or alternatives in real-world applications.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))

Then we import an AcmeClient itself.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
The crucial ACME protocol concept is the Directory. The directory is an URL which allows to fetch all necessary information about ACME server. In our case we're using Letsencrypt staging directory.

Warning

The staging server should be used only for testing purposes. Replace the DIRECTORY variable with the productive endpoint to get the real certificates.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
We define the main function to wrap our code. It assepts the following parameters:

  • email - an account email.
  • client_state - a path to where we can save the state of the client to reuse it later.

Note

The main function is asynchronous

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
The client uses secret key to sign all communications to the server. Later, this key will be bound to account. We use AcmeClient.get_key() function to generate a new key.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
AcmeClient requires two mandatory parameters:

  • ACME Directory URL.
  • The client key.

We use async with construct to initialize the client and make it available within the block.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))

We use new_account() call to register the new account. Since then the client is considered bound and we can use it for other operations.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
Client key and account information is required for any account manipulations. So we save them for later usage.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
Open file for write, note the state has bytes type, so we need to use wb option to write a binary file. Then write our state.

acme_register.py
import asyncio
import sys

from gufo.acme.clients.base import AcmeClient

DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"


async def main(email: str, client_state_path: str) -> None:
    client_key = AcmeClient.get_key()
    async with AcmeClient(DIRECTORY, key=client_key) as client:
        await client.new_account(email)
        state = client.get_state()
    with open(client_state_path, "wb") as fp:
        fp.write(state)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1], sys.argv[2]))
If we're called from command line, get a command line arguments:

  1. Account email
  2. State path

Running

Run the example:

python3 examples/acme_register.py mymail@mydomain.com /tmp/acme.json

Check the /tmp/acme.json file:

/tmp/acme.json
{
  "directory": "https://acme-staging-v02.api.letsencrypt.org/directory",
  "key": {
    "n": "qhd84f-9Wb...5AQQ",
    "e": "AQAB",
    "d": "Sgan5MoDNC..Fk9cw",
    "p": "6BPvgdy6_i..gkdM",
    "q": "u5_dOHJqNh..bRRs",
    "dp": "C9PYRPoG3..MVf9k",
    "dq": "LQ5U14tSS..iRIGU",
    "qi": "5AcvleFCl..jBFsQ"
  },
  "account_url": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/1234567"
}

Conclusions

In this section we have mastered the process of the account registration. Now we're ready to a major ACME's step: A sertificate signing.