[PYTHON] ACME mit Alibaba Cloud verschlüsseln: Erstellen Sie ACME-Endpunkte, Verzeichnisse und ACME-Konten

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 wir den API-Endpunkt

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

ACME-Verzeichnis

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.

Codebeispiel zum Abrufen des ACME-Verzeichnisses

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)

Codebeispiel zum Erstellen eines neuen Kontos

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)

Codebeispiel zum Abrufen von Kontoinformationen

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)

Überblick

[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

ACME mit Alibaba Cloud verschlüsseln: Erstellen Sie ACME-Endpunkte, Verzeichnisse und ACME-Konten
ACME mit Alibaba Cloud verschlüsseln: Kontoschlüssel, Zertifikatschlüssel, Zertifikatsignierungsanforderung erstellen
ACME mit Alibaba Cloud verschlüsseln: Konzepte für SSL-Zertifikate
ACME mit Alibaba Cloud verschlüsseln: Erstellen Sie eine ACME-Anforderung und signieren Sie eine JWS-Nutzlast
Installieren Sie Odoo auf einer Alibaba Cloud ECS-Instanz