Dans cet article en plusieurs parties, vous apprendrez à utiliser l'API Let's Encrypt ACME version 2 avec ** Python ** pour les ** certificats SSL **.
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
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.
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)
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)
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)
[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