In diesem mehrteiligen Artikel erfahren Sie, wie Sie die Let's Encrypt ACME Version 2-API mit ** Python ** für ** SSL-Zertifikate ** verwenden.
Verschlüsseln ACME unterstützt zwei Modi mit unterschiedlichen Endpunkten. Es gibt zwei Modi: den Produktionsmodus, in dem ein echtes Zertifikat ausgestellt und die Rate begrenzt wird, und den Staging-Modus, in dem ein Zertifikat zum Testen ausgestellt wird. Der Produktionsendpunkt begrenzt die Anzahl der Anforderungen, die täglich gestellt werden können. Stellen Sie sicher, dass Sie einen Staging-Endpunkt verwenden, während Sie Software für Let's Encrypt entwickeln.
Staging Endpoint:https://acme-staging-v02.api.letsencrypt.org/directory
Production Endpoint:https://acme-v02.api.letsencrypt.org/directory
Für den ersten API-Aufruf müssen Sie das ACME-Verzeichnis abrufen. Ein Verzeichnis ist eine Liste von URLs, die für verschiedene Befehle aufgerufen werden müssen. Die Antwort aus dem Verzeichnis get hat die folgende JSON-Struktur.
{
"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"
}
Ignoriere die erste Zeile. ACME generiert zufällige Schlüssel und Werte, um zu vermeiden, dass die erwarteten JSON-Werte fest in Ihren Code codiert werden.
Schauen wir uns jeden der zurückgegebenen Datenteile an.
keyChange Diese URL wird verwendet, um den öffentlichen Schlüssel zu ändern, der Ihrem Konto zugeordnet ist. Es wird verwendet, um wichtige Kompromisse zu beheben.
meta.caaIdentities Ein Array von Hostnamen, die der ACME-Server als Referenz für die Überprüfung von CAA-Datensätzen erkennt. Der Beispielcode verwendet diese Datensätze nicht.
meta.termsOfService URL mit den aktuellen Nutzungsbedingungen. [https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf](https://letsencrypt.org/documents/LE-SA-v1.2-November-15- Bitte nehmen Sie sich die Zeit, um das Referenzdokument zu den Nutzungsbedingungen von 2017 zu lesen. Pdf? Im Beispielcode sind die Felder, die die Nutzungsbedingungen akzeptieren, standardmäßig festgelegt.
meta.website URL zur Suche nach Websites, die detaillierte Informationen zum ACME-Server enthalten. Dieser Datensatz wird im Beispielcode nicht verwendet. Weitere Informationen zu diesem Datensatz https://letsencrypt.org/docs/staging-environment/ )
newAccount Dies ist eine wichtige API, und Sie müssen beim Aufrufen der ACME-API einen zweiten Aufruf tätigen. nonce ist ein eindeutiger Zufallswert, der vor Wiederholungsangriffen schützt. Jeder API-Aufruf (außer Verzeichnis) erfordert einen eindeutigen Nonce-Wert.
newNonce Dieser Endpunkt wird verwendet, um ein neues Konto zu erstellen.
newOrder Dieser Endpunkt wird verwendet, um die Ausstellung dieses SSL-Zertifikats anzufordern.
revokeCert Dieser Endpunkt wird verwendet, um ein vorhandenes Zertifikat zu widerrufen, das von demselben Konto ausgestellt wurde.
Dies ist ein Beispiel für die einfachste ACME-API im Beispielpaket. In diesem Beispiel rufen wir einfach den ACME-Hauptendpunkt auf. Die zurückgegebenen Daten sind eine JSON-Struktur, die den API-Endpunkt wie oben beschrieben definiert. Überprüfen Sie die Ausgabe dieses Programms und machen Sie sich mit den verschiedenen URLs vertraut, die in den meisten der folgenden Beispiele verwendet werden.
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)
Der nächste Schritt besteht darin, ein neues Konto auf dem ACME-Server zu erstellen. Dies beinhaltet [Teil 2](https://www.alibabacloud.com/blog/let%27s-encrypt-acme-with-alibaba-cloud-api-gateway-and-cdn-%E2%80%93- Verwenden Sie den in Teil-2_593778 erstellten Kontoschlüssel (Spm = a2c65.11461447.0.0.7a192881kqOoxn). Der ACME-Server verfolgt keine Informationen wie den Firmennamen in der Kontodatenbank.
Ändern Sie in diesem Beispiel den Parameter EmailAddress in Ihre eigene E-Mail-Adresse. Dieses Beispiel zeigt, wie mehrere E-Mail-Adressen eingeschlossen werden. Die Eingabe einer E-Mail-Adresse ist auf dem ACME-Server optional und nicht erforderlich. Der ACME-Server überprüft Ihre E-Mail-Adresse nicht.
Lassen Sie uns einige wichtige Punkte zu diesem Code überprüfen.
** 1. Holen Sie sich das ACME-Verzeichnis **
acme_config = get_directory()
** 2. Holen Sie sich die URL von "neues Konto" **
url = acme_config["newAccount"]
** 3. Nonce für den ersten ACME-API-Aufruf anfordern ** Nach dem ersten ACME-API-Aufruf wird nach jedem ACME-API-Aufruf eine neue Nonce im Header "Replay-Nonce" zurückgegeben.
nonce = requests.head(acme_config['newNonce']).headers['Replay-Nonce']
** 4. Stellen Sie den HTML-Header zusammen ** Das wichtige Element ist Inhaltstyp: application / jose + json
headers = {
'User-Agent': 'neoprime.io-acme-client/1.0',
'Accept-Language': 'en',
'Content-Type': 'application/jose+json'
}
** 5. Stellen Sie den HTML-Body mit ACME-API-Parametern zusammen ** Das Erstellen eines HTTP-Körpers wird in Teil 4 ausführlich erläutert.
payload = {}
payload["termsOfServiceAgreed"] = True
payload["contact"] = EmailAddresses
body_top = {
"alg": "RS256",
"jwk": myhelper.get_jwk(AccountKeyFile),
"url": url,
"nonce": nonce
}
** 6. Stellen Sie die Datenstruktur des HTML-Körpers "jose" zusammen ** Beachten Sie, dass alles base64-codiert ist.
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. Rufen Sie abschließend die ACME-API auf. ** ** ** Dies erfolgt über HTTP POST mit einem JSON-Body.
resp = requests.post(url, json=jose, headers=headers)
** 8. Nach der ACME-API werden zwei Elemente im HTTP-Antwortheader zurückgegeben. ** ** ** Standort ist die URL Ihres Kontos.
Replay-Nonce ist der "Nonce" -Wert für den nächsten ACME-API-Aufruf.
resp.headers['Location']
resp.headers['Replay-Nonce']
Bei den meisten ACME-API-Aufrufen muss der HTTP-Header enthalten sein.
Content-Type: application/jose+json
** Quelle: 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)
Nachdem Sie mit account.key ein Konto erstellt haben, kommunizieren wir mit dem ACME-Server, um festzustellen, welche Informationen auf dem Server gespeichert sind. In diesem Beispiel wird die Konfigurationsdatei "acme.ini" eingeführt, anstatt die Konfigurationsparameter fest in den Quellcode zu codieren.
Ändern Sie acme.ini so, dass bestimmte Informationen wie Ihre E-Mail-Adresse enthalten sind.
** Quelle: 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
** Quelle: 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)
[Teil 4](https://www.alibabacloud.com/blog/let%27s-encrypt-acme-with-alibaba-cloud-api-gateway-and-cdn-%E2%80%93-part-4_593786? In spm = a2c65.11461447.0.0.7a192881oOb2lp) werden Sie tiefer in die ACME-API eintauchen, um zu erfahren, wie Sie jeden Teil des JSON-Körpers erstellen, die Nutzdaten signieren und die Ergebnisse verarbeiten.
Recommended Posts