convert / dehydrated / certbot / letsencrypt config

convert / dehydrated / certbot / letsencrypt config

  • Written by
    Walter Doekes
  • Published on

If you find yourself in the situation that you have to reuse your Letsencrypt credentials/account generated by Dehydrated (a bash Letsencrypt interface) with the official Certbot client, like me, you’ll want to convert your config files.

In my case, I wanted to change my e-mail address, and the Dehydrated client offered no such command. With Certbot you can do this:

$ certbot register --update-registration --account f65c...

But you’ll need your credentials in a format that Certbot groks.

With a bit of trial and error, you can came a long way converting the files:

$ ls /.../dehydrated/accounts/ACCOUNT
account_key.pem  registration_info.json

$ mkdir -p /etc/letsencrypt/accounts/
$ cd /etc/letsencrypt/accounts/
$ cat >meta.json <<EOF
{"creation_host": "", "creation_dt": "2016-12-20T14:12:31Z"}

If you have a sample Certbot regr.json, you’ll figure out what to place there based on the contents of the Dehydrated registration_info.json.


  "key": {
    "kty": "RSA",
    "n": "MODULUS_IN_BASE64",
    "e": "EXPONENT_IN_BASE64"
  "contact": [],
  "agreement": "",
  "initialIp": "IP_ADDRESS",
  "createdAt": "2016-12-20T14:12:31.054249908Z",
  "Status": "valid"


  "body": {
    "contact": [],
    "agreement": "",
    "key": {
      "kty": "RSA",
      "n": "MODULUS_IN_BASE64",
      "e": "EXPONENT_IN_BASE64"
  "uri": "",
  "new_authzr_uri": "",
  "terms_of_service": ""

Lastly, you’ll need the Certbot private_key.json. It can be converted from Dehydrated account_key.pem, with the following python snippet:

#!/usr/bin/env python
# Usage: openssl rsa -in account_key.pem -text -noout | python
# Will convert the RSA PEM private key to the Letsencrypt/Certbot
# private_key.json file.
# Public Domain, Walter Doekes, OSSO B.V., 2016
# From:
#     -----BEGIN RSA PRIVATE KEY-----
#     MIIJJwsdAyjCseEAtNsljpkjhk9143w//jVdsfWsdf9sffLgdsf+sefdfsgE54km
#     ...
# To:
#     {"e": "AQAB",
#      "n": "2YIitsUxJlYn_rVn_8Sges...",
#     ...
from base64 import b64encode
from sys import stdin

maps = {
    'modulus': 'n', 'privateExponent': 'd', 'prime1': 'p', 'prime2': 'q',
    'coefficient': 'qi', 'exponent1': 'dp', 'exponent2': 'dq'}
extra = {'kty': 'RSA', 'e': '<publicExponent>'}

def block2b64(lines, key):
    found = False
    chars = []
    for line in lines:
        if line.startswith(key + ':'):
            found = True
        elif found and line.startswith(' '):
            for i in line.split(':'):
                i = i.strip()
                if i:
                    chars.append(chr(int(i, 16)))
        elif found:
    assert chars, 'nothing found for {0}'.format(key)
    return b64encode(''.join(chars))

data ='\n')
conv = dict((v, block2b64(data, k)) for k, v in maps.items())

# Add exponent
e = [i for i in data if i.startswith('publicExponent:')][0]
e = e.split('(', 1)[-1].split(')', 1)[0]
assert e.startswith('0x'), e
e = ('', '0')[len(e) % 2 == 1] + e[2:]
e = b64encode(''.join(chr(int(e[i:i+2], 16)) for i in range(0, len(e), 2)))
conv['e'] = e

# JSON-safe output.
print(repr(conv).replace("'", '"'))

Don’t forget to chmod 400 the private_key.json.

Back to overview Newer post: detect invisible selection / copy buffer / chrome Older post: mysql / deterministic / reads sql data