[PYTHON] Cryptez ACME avec Alibaba Cloud: créez des points de terminaison, des répertoires et des comptes ACME ACME

Dans cet article en plusieurs parties, vous apprendrez à utiliser l'API Let's Encrypt ACME version 2 avec ** Python ** pour les ** certificats SSL **.

Cryptons le point de terminaison de l'API

Let's Encrypt ACME prend en charge deux modes avec des points de terminaison différents. Il existe deux modes: le mode production, qui délivre un certificat réel et limite le débit, et le mode intermédiaire, qui délivre un certificat pour les tests. Le point de terminaison de production limite le nombre de demandes pouvant être effectuées chaque jour. Assurez-vous que vous utilisez un point de terminaison intermédiaire lors du développement du logiciel pour Let's Encrypt.

Staging Endpoint:https://acme-staging-v02.api.letsencrypt.org/directory

Production Endpoint:https://acme-v02.api.letsencrypt.org/directory

Annuaire ACME

Le premier appel API vous oblige à obtenir le répertoire ACME. Un répertoire est une liste d'URL à appeler pour diverses commandes. La réponse du répertoire get a la structure JSON suivante.

{
    "LPTIN-Jj4u0": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
    "keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change",
    "meta": {
        "caaIdentities": [
            "letsencrypt.org"
        ],
        "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
        "website": "https://letsencrypt.org/docs/staging-environment/"
    },
    "newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
    "newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
    "newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
    "revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert"
}

Ignorez la première ligne. ACME génère des clés et des valeurs aléatoires pour éviter de coder en dur les valeurs JSON attendues dans votre code.

Examinons chacune des parties de données renvoyées.

keyChange Cette URL est utilisée pour modifier la clé publique associée à votre compte. Il est utilisé pour récupérer d'un compromis clé.

meta.caaIdentities Un tableau de noms d'hôtes que le serveur ACME reconnaît comme se référant à lui-même pour la vérification des enregistrements CAA. L'exemple de code n'utilise pas ces enregistrements.

meta.termsOfService URL indiquant les conditions d'utilisation actuelles. [https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf](https://letsencrypt.org/documents/LE-SA-v1.2-November-15- Veuillez prendre le temps de lire le document de référence concernant les conditions d'utilisation de 2017.pdf? Spm = a2c65.11461447.0.0.7a192881Lr9stV & file = LE-SA-v1.2-novembre-15-2017.pdf) Dans l'exemple de code, les champs qui acceptent les conditions d'utilisation sont définis par défaut.

meta.website URL pour rechercher des sites Web fournissant des informations détaillées sur le serveur ACME. Cet enregistrement n'est pas utilisé dans l'exemple de code. En savoir plus sur cet enregistrement https://letsencrypt.org/docs/staging-environment/

newAccount Il s'agit d'une API importante et vous devez effectuer un deuxième appel lors de l'appel de l'API ACME. nonce est une valeur aléatoire unique qui protège contre les attaques de relecture. Chaque appel d'API (à l'exception du répertoire) nécessite une valeur nonce unique.

newNonce Ce point de terminaison sera utilisé pour créer un nouveau compte.

newOrder Ce point de terminaison est utilisé pour demander l'émission de ce certificat SSL.

revokeCert Ce point de terminaison est utilisé pour révoquer un certificat existant émis par le même compte.

Exemple de code pour obtenir le répertoire ACME

Ceci est un exemple de l'API ACME la plus simple dans le package d'exemples. Dans cet exemple, nous appelons simplement le point de terminaison principal ACME. Les données renvoyées sont une structure JSON qui définit le point de terminaison de l'API comme décrit ci-dessus. Vérifiez la sortie de ce programme et familiarisez-vous avec les différentes URL utilisées dans la plupart des exemples ci-dessous.

Source: get_directory.py

""" Let's Encrypt ACME Version 2 Examples - Get Directory"""

# This example will call the ACME API directory and display the returned data
# Reference: https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.7.1.1

import    sys
import    requests
import    helper

path = 'https://acme-staging-v02.api.letsencrypt.org/directory'

headers = {
    'User-Agent': 'neoprime.io-acme-client/1.0',
    'Accept-Language': 'en'
}

try:
    print('Calling endpoint:', path)
    directory = requests.get(path, headers=headers)
except requests.exceptions.RequestException as error:
    print(error)
    sys.exit(1)

if directory.status_code < 200 or directory.status_code >= 300:
    print('Error calling ACME endpoint:', directory.reason)
    sys.exit(1)

# The output should be json. If not something is wrong
try:
    acme_config = directory.json()
except Exception as ex:
    print("Error: Cannot load returned data:", ex)
    sys.exit(1)

print('')
print('Returned Data:')
print('****************************************')
print(directory.text)

acme_config = directory.json()

print('')
print('Formatted JSON:')
print('****************************************')
helper.print_dict(acme_config, 0)

Exemple de code pour créer un nouveau compte

L'étape suivante consiste à créer un nouveau compte sur le serveur ACME. Cela inclut [Partie 2](https://www.alibabacloud.com/blog/let%27s-encrypt-acme-with-alibaba-cloud-api-gateway-and-cdn-%E2%80%93- Utilisez le account.key créé dans part-2_593778? Spm = a2c65.11461447.0.0.7a192881kqOoxn). Le serveur ACME ne suit pas les informations telles que le nom de la société dans la base de données des comptes.

Dans cet exemple, remplacez le paramètre EmailAddress par votre propre adresse e-mail. Cet exemple montre comment inclure plusieurs adresses e-mail. La saisie d'une adresse e-mail est facultative sur le serveur ACME et n'est pas obligatoire. Le serveur ACME ne vérifie pas votre adresse e-mail.

Passons en revue quelques points importants sur ce code.

** 1. Obtenez le répertoire ACME **

    acme_config = get_directory()

** 2. Obtenez l'URL du "nouveau compte" **

    url = acme_config["newAccount"]

** 3. Demander un nonce pour le premier appel d'API ACME ** Après le premier appel d'API ACME, un nouveau nonce est renvoyé dans l'en-tête "Replay-Nonce" après chaque appel d'API ACME.

    nonce = requests.head(acme_config['newNonce']).headers['Replay-Nonce']

** 4. Assemblez l'en-tête HTML ** L'élément important est Content-Type: application / jose + json

    headers = {
        'User-Agent': 'neoprime.io-acme-client/1.0',
        'Accept-Language': 'en',
        'Content-Type': 'application/jose+json'
    }

** 5. Assemblez le corps HTML avec les paramètres de l'API ACME ** La création d'un corps HTTP sera expliquée en détail dans la partie 4.

    payload = {}

    payload["termsOfServiceAgreed"] = True
    payload["contact"] = EmailAddresses

    body_top = {
        "alg": "RS256",
        "jwk": myhelper.get_jwk(AccountKeyFile),
        "url": url,
        "nonce": nonce
    }

** 6. Assemblez la structure de données du corps HTML "jose" ** Notez que tout est encodé en base64.

    body_top_b64 = myhelper.b64(json.dumps(body_top).encode("utf8"))
    payload_b64 = myhelper.b64(json.dumps(payload).encode("utf8"))

    jose = {
        "protected": body_top_b64,
        "payload": payload_b64,
        "signature": myhelper.b64(signature)
    }

** 7. Enfin, appelez l'API ACME. ** ** Cela se fait par HTTP POST avec un corps JSON.

    resp = requests.post(url, json=jose, headers=headers)

** 8. Après l'API ACME, deux éléments sont renvoyés dans l'en-tête de réponse HTTP. ** ** L'emplacement est l'URL de votre compte.

Replay-Nonce est la valeur "nonce" pour le prochain appel d'API ACME.

    resp.headers['Location']
    resp.headers['Replay-Nonce']

La plupart des appels d'API ACME nécessitent l'inclusion de l'en-tête HTTP.

Content-Type: application/jose+json

** Source: new_account.py **

""" Let's Encrypt ACME Version 2 Examples - New Account"""

# This example will call the ACME API directory and create a new account
# Reference: https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.7.3.2

import    os
import    sys
import    json
import    requests
import    myhelper

# Staging URL
path = 'https://acme-staging-v02.api.letsencrypt.org/directory'

# Production URL
# path = 'https://acme-v02.api.letsencrypt.org/directory'

AccountKeyFile = 'account.key'

EmailAddresses = ['mailto:[email protected]', 'mailto:[email protected]']

def check_account_key_file():
    """ Verify that the Account Key File exists and prompt to create if it does not exist """
    if os.path.exists(AccountKeyFile) is not False:
        return True

    print('Error: File does not exist: {0}'.format(AccountKeyFile))

    if myhelper.Confirm('Create new account private key (y/n): ') is False:
        print('Cancelled')
        return False

    myhelper.create_rsa_private_key(AccountKeyFile)

    if os.path.exists(AccountKeyFile) is False:
        print('Error: File does not exist: {0}'.format(AccountKeyFile))
        return False

    return True

def get_directory():
    """ Get the ACME Directory """
    headers = {
        'User-Agent': 'neoprime.io-acme-client/1.0',
        'Accept-Language': 'en',
    }

    try:
        print('Calling endpoint:', path)
        directory = requests.get(path, headers=headers)
    except requests.exceptions.RequestException as error:
        print(error)
        return False

    if directory.status_code < 200 or directory.status_code >= 300:
        print('Error calling ACME endpoint:', directory.reason)
        print(directory.text)
        return False

    # The following statements are to understand the output
    acme_config = directory.json()

    return acme_config

def main():
    """ Main Program Function """
    headers = {
        'User-Agent': 'neoprime.io-acme-client/1.0',
        'Accept-Language': 'en',
        'Content-Type': 'application/jose+json'
    }

    if check_account_key_file() is False:
        sys.exit(1)

    acme_config = get_directory()

    if acme_config is False:
        sys.exit(1)

    url = acme_config["newAccount"]

    # Get the URL for the terms of service
    terms_service = acme_config.get("meta", {}).get("termsOfService", "")
    print('Terms of Service:', terms_service)

    nonce = requests.head(acme_config['newNonce']).headers['Replay-Nonce']
    print('Nonce:', nonce)
    print("")

    # Create the account request
    payload = {}

    if terms_service != "":
        payload["termsOfServiceAgreed"] = True

    payload["contact"] = EmailAddresses

    payload_b64 = myhelper.b64(json.dumps(payload).encode("utf8"))

    body_top = {
        "alg": "RS256",
        "jwk": myhelper.get_jwk(AccountKeyFile),
        "url": url,
        "nonce": nonce
    }

    body_top_b64 = myhelper.b64(json.dumps(body_top).encode("utf8"))

    data = "{0}.{1}".format(body_top_b64, payload_b64).encode("utf8")

    signature = myhelper.sign(data, AccountKeyFile)

    #
    # Create the HTML request body
    #

    jose = {
        "protected": body_top_b64,
        "payload": payload_b64,
        "signature": myhelper.b64(signature)
    }

    try:
        print('Calling endpoint:', url)
        resp = requests.post(url, json=jose, headers=headers)
    except requests.exceptions.RequestException as error:
        resp = error.response
        print(resp)
    except Exception as ex:
        print(ex)
    except BaseException as ex:
        print(ex)

    if resp.status_code < 200 or resp.status_code >= 300:
        print('Error calling ACME endpoint:', resp.reason)
        print('Status Code:', resp.status_code)
        myhelper.process_error_message(resp.text)
        sys.exit(1)

    print('')
    if 'Location' in resp.headers:
        print('Account URL:', resp.headers['Location'])
    else:
        print('Error: Response headers did not contain the header "Location"')

main()

sys.exit(0)

Exemple de code pour obtenir des informations de compte

Maintenant que vous avez créé un compte à l'aide de account.key, communiquons avec le serveur ACME pour voir quelles informations sont stockées sur le serveur. Dans cet exemple, au lieu de coder en dur les paramètres de configuration dans le code source, le fichier de configuration "acme.ini" est introduit.

Modifiez acme.ini pour inclure certaines informations telles que votre adresse e-mail.

** Source: acme.ini **

 
[acme-neoprime]
UserAgent = neoprime.io-acme-client/1.0

# [Required] ACME account key
AccountKeyFile = account.key

# Certifcate Signing Request (CSR)
CSRFile = example.com.csr

ChainFile = example.com.chain.pem

# ACME URL
# Staging URL
# https://acme-staging-v02.api.letsencrypt.org/directory
# Production URL
# https://acme-v02.api.letsencrypt.org/directory

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

# Email Addresses so that LetsEncrypt can notify about SSL renewals
Contacts = mailto:example.com;mailto:[email protected]

# Preferred Language
Language = en

** Source: get_acount_info.py **

""" Let's Encrypt ACME Version 2 Examples - Get Account Information """

############################################################
# This example will call the ACME API directory and get the account information
# Reference: https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.7.3.3
#
# This program uses the AccountKeyFile set in acme.ini to return information about the ACME account.
############################################################

import    sys
import    json
import    requests
import    helper
import    myhelper

############################################################
# Start - Global Variables

g_debug = 0

acme_path = ''
AccountKeyFile = ''
EmailAddresses = []
headers = {}

# End - Global Variables
############################################################

############################################################
# Load the configuration from acme.ini
############################################################


def load_acme_parameters(debug=0):
    """ Load the configuration from acme.ini """

    global acme_path
    global AccountKeyFile
    global EmailAddresses
    global headers

    config = myhelper.load_acme_config(filename='acme.ini')

    if debug is not 0:
        print(config.get('acme-neoprime', 'accountkeyfile'))
        print(config.get('acme-neoprime', 'csrfile'))
        print(config.get('acme-neoprime', 'chainfile'))
        print(config.get('acme-neoprime', 'acmedirectory'))
        print(config.get('acme-neoprime', 'contacts'))
        print(config.get('acme-neoprime', 'language'))

    acme_path = config.get('acme-neoprime', 'acmedirectory')

    AccountKeyFile = config.get('acme-neoprime', 'accountkeyfile')

    EmailAddresses = config.get('acme-neoprime', 'contacts').split(';')

    headers['User-Agent'] = config.get('acme-neoprime', 'UserAgent')
    headers['Accept-Language'] = config.get('acme-neoprime', 'language')
    headers['Content-Type'] = 'application/jose+json'

    return config

############################################################
#
############################################################

def get_account_url(url, nonce):
    """ Get the Account URL based upon the account key """

    # Create the account request
    payload = {}

    payload["termsOfServiceAgreed"] = True
    payload["contact"] = EmailAddresses
    payload["onlyReturnExisting"] = True

    payload_b64 = myhelper.b64(json.dumps(payload).encode("utf8"))

    body_top = {
        "alg": "RS256",
        "jwk": myhelper.get_jwk(AccountKeyFile),
        "url": url,
        "nonce": nonce
    }

    body_top_b64 = myhelper.b64(json.dumps(body_top).encode("utf8"))

    #
    # Create the message digest
    #

    data = "{0}.{1}".format(body_top_b64, payload_b64).encode("utf8")

    signature = myhelper.sign(data, AccountKeyFile)

    #
    # Create the HTML request body
    #

    jose = {
        "protected": body_top_b64,
        "payload": payload_b64,
        "signature": myhelper.b64(signature)
    }

    #
    # Make the ACME request
    #

    try:
        print('Calling endpoint:', url)
        resp = requests.post(url, json=jose, headers=headers)
    except requests.exceptions.RequestException as error:
        resp = error.response
        print(resp)
    except Exception as error:
        print(error)

    if resp.status_code < 200 or resp.status_code >= 300:
        print('Error calling ACME endpoint:', resp.reason)
        print('Status Code:', resp.status_code)
        myhelper.process_error_message(resp.text)
        sys.exit(1)

    if 'Location' in resp.headers:
        print('Account URL:', resp.headers['Location'])
    else:
        print('Error: Response headers did not contain the header "Location"')

    # Get the nonce for the next command request
    nonce = resp.headers['Replay-Nonce']

    account_url = resp.headers['Location']

    return nonce, account_url

############################################################
#
############################################################

def get_account_info(nonce, url, location):
    """ Get the Account Information """

    # Create the account request
    payload = {}

    payload_b64 = myhelper.b64(json.dumps(payload).encode("utf8"))

    body_top = {
        "alg": "RS256",
        "kid": location,
        "nonce": nonce,
        "url": location
    }

    body_top_b64 = myhelper.b64(json.dumps(body_top).encode("utf8"))

    #
    # Create the message digest
    #

    data = "{0}.{1}".format(body_top_b64, payload_b64).encode("utf8")

    signature = myhelper.sign(data, AccountKeyFile)

    #
    # Create the HTML request body
    #

    jose = {
        "protected": body_top_b64,
        "payload": payload_b64,
        "signature": myhelper.b64(signature)
    }

    #
    # Make the ACME request
    #

    try:
        print('Calling endpoint:', url)
        resp = requests.post(url, json=jose, headers=headers)
    except requests.exceptions.RequestException as error:
        resp = error.response
        print(resp)
    except Exception as error:
        print(error)

    if resp.status_code < 200 or resp.status_code >= 300:
        print('Error calling ACME endpoint:', resp.reason)
        print('Status Code:', resp.status_code)
        myhelper.process_error_message(resp.text)
        sys.exit(1)

    nonce = resp.headers['Replay-Nonce']

    # resp.text is the returned JSON data describing the account

    return nonce, resp.text

############################################################
#
############################################################

def load_acme_urls(path):
    """ Load the ACME Directory of URLS """
    try:
        print('Calling endpoint:', path)
        resp = requests.get(acme_path, headers=headers)
    except requests.exceptions.RequestException as error:
        print(error)
        sys.exit(1)

    if resp.status_code < 200 or resp.status_code >= 300:
        print('Error calling ACME endpoint:', resp.reason)
        print(resp.text)
        sys.exit(1)

    return resp.json()

############################################################
#
############################################################

def acme_get_nonce(urls):
    """ Get the ACME Nonce that is used for the first request """
    global    headers

    path = urls['newNonce']

    try:
        print('Calling endpoint:', path)
        resp = requests.head(path, headers=headers)
    except requests.exceptions.RequestException as error:
        print(error)
        return False

    if resp.status_code < 200 or resp.status_code >= 300:
        print('Error calling ACME endpoint:', resp.reason)
        print(resp.text)
        return False

    return resp.headers['Replay-Nonce']

############################################################
# Main Program Function
############################################################

def main(debug=0):
    """ Main Program Function """
    acme_urls = load_acme_urls(acme_path)

    url = acme_urls["newAccount"]

    nonce = acme_get_nonce(acme_urls)

    if nonce is False:
        sys.exit(1)

    nonce, account_url = get_account_url(url, nonce)

    # resp is the returned JSON data describing the account
    nonce, resp = get_account_info(nonce, account_url, account_url)

    info = json.loads(resp)

    if debug is not 0:
        print('')
        print('Returned Data:')
        print('##################################################')
        #print(info)
        helper.print_dict(info)
        print('##################################################')

    print('')
    print('ID:        ', info['id'])
    print('Contact:   ', info['contact'])
    print('Initial IP:', info['initialIp'])
    print('Created At:', info['createdAt'])
    print('Status:   ', info['status'])

def is_json(data):
    try:
        json.loads(data)
    except ValueError as e:
        return False
    return True

acme_config = load_acme_parameters(g_debug)

main(g_debug)

Aperçu

[Partie 4](https://www.alibabacloud.com/blog/let%27s-encrypt-acme-with-alibaba-cloud-api-gateway-and-cdn-%E2%80%93-part-4_593786? Dans spm = a2c65.11461447.0.0.7a192881oOb2lp), vous approfondirez l'API ACME pour apprendre à créer chaque partie du corps JSON, signer la charge utile et traiter les résultats.

Recommended Posts

Cryptez ACME avec Alibaba Cloud: créez des points de terminaison, des répertoires et des comptes ACME ACME
Crypter ACME avec Alibaba Cloud: créer une clé de compte, une clé de certificat, une demande de signature de certificat
Crypter ACME avec Alibaba Cloud: concepts liés aux certificats SSL
Crypter ACME avec Alibaba Cloud: créez une requête ACME et signez une charge utile JWS
Installez Odoo sur une instance Alibaba Cloud ECS