From 523f8998c25f90c4718b7591ab90da9c08c16d33 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Thu, 3 Apr 2025 11:27:00 +0000 Subject: [PATCH] Admin: Use cryptography instead of OpenSSL --- localstack-core/localstack/utils/crypto.py | 116 ++++++++++++++------- pyproject.toml | 2 - requirements-base-runtime.txt | 4 +- requirements-dev.txt | 4 +- requirements-runtime.txt | 5 +- requirements-test.txt | 4 +- 6 files changed, 83 insertions(+), 52 deletions(-) diff --git a/localstack-core/localstack/utils/crypto.py b/localstack-core/localstack/utils/crypto.py index bd7150d96b871..766d29473d014 100644 --- a/localstack-core/localstack/utils/crypto.py +++ b/localstack-core/localstack/utils/crypto.py @@ -1,11 +1,16 @@ import io +import ipaddress import logging import os import re import threading +from datetime import datetime, timedelta, timezone from typing import Tuple +from cryptography import x509 from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from .files import TMP_FILES, file_exists_not_empty, load_file, new_tmp_file, save_file @@ -27,6 +32,10 @@ PEM_KEY_START_REGEX = r"-----BEGIN(.*)PRIVATE KEY-----" PEM_KEY_END_REGEX = r"-----END(.*)PRIVATE KEY-----" +IPV4_REGEX = re.compile( + r"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}" +) + @synchronized(lock=SSL_CERT_LOCK) def generate_ssl_cert( @@ -36,10 +45,6 @@ def generate_ssl_cert( return_content=False, serial_number=None, ): - # Note: Do NOT import "OpenSSL" at the root scope - # (Our test Lambdas are importing this file but don't have the module installed) - from OpenSSL import crypto - def all_exist(*files): return all(os.path.exists(f) for f in files) @@ -80,48 +85,85 @@ def store_cert_key_files(base_filename): target_file = "%s.%s" % (target_file, short_uid()) # create a key pair - k = crypto.PKey() - k.generate_key(crypto.TYPE_RSA, 2048) + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) host_definition = localstack_host() - # create a self-signed cert - cert = crypto.X509() - subj = cert.get_subject() - subj.C = "AU" - subj.ST = "Some-State" - subj.L = "Some-Locality" - subj.O = "LocalStack Org" # noqa - subj.OU = "Testing" - subj.CN = "localhost" - # Note: new requirements for recent OSX versions: https://support.apple.com/en-us/HT210176 - # More details: https://www.iol.unh.edu/blog/2019/10/10/macos-catalina-and-chrome-trust - serial_number = serial_number or 1001 - cert.set_version(2) - cert.set_serial_number(serial_number) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(2 * 365 * 24 * 60 * 60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(k) - alt_names = ( - f"DNS:localhost,DNS:test.localhost.atlassian.io,DNS:localhost.localstack.cloud,DNS:{host_definition.host}IP:127.0.0.1" - ).encode("utf8") - cert.add_extensions( + issuer = x509.Name( [ - crypto.X509Extension(b"subjectAltName", False, alt_names), - crypto.X509Extension(b"basicConstraints", True, b"CA:false"), - crypto.X509Extension( - b"keyUsage", True, b"nonRepudiation,digitalSignature,keyEncipherment" - ), - crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"), + x509.NameAttribute(x509.NameOID.COUNTRY_NAME, "AU"), + x509.NameAttribute(x509.NameOID.STATE_OR_PROVINCE_NAME, "Some-State"), + x509.NameAttribute(x509.NameOID.LOCALITY_NAME, "Some-Locality"), + x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, "LocalStack Org"), + x509.NameAttribute(x509.NameOID.ORGANIZATIONAL_UNIT_NAME, "Testing"), + x509.NameAttribute(x509.NameOID.COMMON_NAME, "cryptography.io"), ] ) - cert.sign(k, "SHA256") + + # create a self-signed cert + public_key = private_key.public_key() + builder = ( + x509.CertificateBuilder() + .subject_name(issuer) + .issuer_name(issuer) + .public_key(public_key) + .serial_number(serial_number or 1001) + .not_valid_before(datetime.now(tz=timezone.utc)) + .not_valid_after(datetime.now(tz=timezone.utc) + timedelta(days=365 * 2)) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) + .add_extension( + x509.KeyUsage( + crl_sign=False, + key_cert_sign=False, + digital_signature=True, + content_commitment=False, + key_encipherment=True, + data_encipherment=False, + key_agreement=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(public_key), + critical=False, + ) + ) + + dns = [ + "localhost", + "test.localhost.atlassian.io", + "localhost.localstack.cloud", + host_definition.host, + "127.0.0.1", + ] + # SSL treats IP addresses differently from regular host names + # https://cabforum.org/working-groups/server/guidance-ip-addresses-certificates/ + x509_names_or_ips = [ + x509.IPAddress(ipaddress.IPv4Address(name)) + if IPV4_REGEX.match(name) + else x509.DNSName(name) + for name in dns + ] + builder = builder.add_extension(x509.SubjectAlternativeName(x509_names_or_ips), critical=False) + + builder = builder.add_extension( + x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]), critical=True + ) + + cert = builder.sign(private_key, hashes.SHA256()) + + private_key_bytes = private_key.private_bytes( + serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) cert_file = io.StringIO() key_file = io.StringIO() - cert_file.write(to_str(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))) - key_file.write(to_str(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))) + cert_file.write(to_str(cert.public_bytes(serialization.Encoding.PEM))) + key_file.write(to_str(private_key_bytes)) cert_file_content = cert_file.getvalue().strip() key_file_content = key_file.getvalue().strip() file_content = "%s\n%s" % (key_file_content, cert_file_content) diff --git a/pyproject.toml b/pyproject.toml index cc1a5be68b614..77be694f66c09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,6 @@ base-runtime = [ "hypercorn>=0.14.4", "localstack-twisted>=23.0", "openapi-core>=0.19.2", - "pyopenssl>=23.0.0", "readerwriterlock>=1.0.7", "requests-aws4auth>=1.0", # explicitly set urllib3 to force its usage / ensure compatibility @@ -95,7 +94,6 @@ runtime = [ "moto-ext[all]==5.1.3.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", - "pyopenssl>=23.0.0", ] # for running tests and coverage analysis diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index ec58c4c368b22..9a3da8b89e2c8 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -131,9 +131,7 @@ pycparser==2.22 pygments==2.19.1 # via rich pyopenssl==25.0.0 - # via - # localstack-core (pyproject.toml) - # localstack-twisted + # via localstack-twisted pyproject-hooks==1.2.0 # via build python-dateutil==2.9.0.post0 diff --git a/requirements-dev.txt b/requirements-dev.txt index 82d230cb8e48a..bbd6e7102cbf3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -340,9 +340,7 @@ pygments==2.19.1 pymongo==4.12.0 # via localstack-core pyopenssl==25.0.0 - # via - # localstack-core - # localstack-twisted + # via localstack-twisted pypandoc==1.15 # via localstack-core (pyproject.toml) pyparsing==3.2.3 diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 378165d67c158..af0fb8e085b81 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -248,10 +248,7 @@ pygments==2.19.1 pymongo==4.12.0 # via localstack-core (pyproject.toml) pyopenssl==25.0.0 - # via - # localstack-core - # localstack-core (pyproject.toml) - # localstack-twisted + # via localstack-twisted pyparsing==3.2.3 # via moto-ext pyproject-hooks==1.2.0 diff --git a/requirements-test.txt b/requirements-test.txt index 67715c62c9c7d..2c12200a34b2d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -310,9 +310,7 @@ pygments==2.19.1 pymongo==4.12.0 # via localstack-core pyopenssl==25.0.0 - # via - # localstack-core - # localstack-twisted + # via localstack-twisted pyparsing==3.2.3 # via moto-ext pyproject-hooks==1.2.0