-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration tests for OpenSSL-linking 3p modules (#1587)
This change expands our python integration tests to load and exercise libcrypto-linking 3rd party modules. Cases covered include statically linked AWS-LC (default AWS CRT) and dynamically linked OpenSSL (AWS CRT, PyOpenSSL, PyCA). We also test these integrations with python's AWS-LC built in FIPS mode. Due to the pre-release nature of python 3.13 and 3.14, there are a few caveats for later versions: 3.12 introduced [a change](python/cpython#95299) to drop `setuptools` from default virtual environments, causing installation issues for PyCA and PyOpenSSL on 3.13+. To work around this, we allow installation failure for those dependencies on newer (currently both 3.13 and 3.14 are pre-release) and exit early from the relevant test cases. We also encountered some issues installing CRT bindings from source on 3.12+, so for those versions we install the pre-compiled wheel module from PyPI, which uses AWS-LC under the hood.
- Loading branch information
1 parent
4b07805
commit 4013299
Showing
6 changed files
with
481 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
262 changes: 262 additions & 0 deletions
262
tests/ci/integration/python_patch/3.13/aws-lc-cpython.patch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py | ||
index 6e63a88..7dc83d7 100644 | ||
--- a/Lib/test/test_httplib.py | ||
+++ b/Lib/test/test_httplib.py | ||
@@ -2066,7 +2066,7 @@ def test_host_port(self): | ||
|
||
def test_tls13_pha(self): | ||
import ssl | ||
- if not ssl.HAS_TLSv1_3: | ||
+ if not ssl.HAS_TLSv1_3 or "AWS-LC" in ssl.OPENSSL_VERSION: | ||
self.skipTest('TLS 1.3 support required') | ||
# just check status of PHA flag | ||
h = client.HTTPSConnection('localhost', 443) | ||
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py | ||
index 0e50d09..f4b7b3c 100644 | ||
--- a/Lib/test/test_ssl.py | ||
+++ b/Lib/test/test_ssl.py | ||
@@ -41,6 +41,7 @@ | ||
from ssl import Purpose, TLSVersion, _TLSContentType, _TLSMessageType, _TLSAlertType | ||
|
||
Py_DEBUG_WIN32 = support.Py_DEBUG and sys.platform == 'win32' | ||
+Py_OPENSSL_IS_AWSLC = "AWS-LC" in ssl.OPENSSL_VERSION | ||
|
||
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) | ||
HOST = socket_helper.HOST | ||
@@ -174,7 +175,7 @@ def is_ubuntu(): | ||
except FileNotFoundError: | ||
return False | ||
|
||
-if is_ubuntu(): | ||
+if is_ubuntu() and not Py_OPENSSL_IS_AWSLC: | ||
def seclevel_workaround(*ctxs): | ||
""""Lower security level to '1' and allow all ciphers for TLS 1.0/1""" | ||
for ctx in ctxs: | ||
@@ -4001,6 +4002,7 @@ def test_no_legacy_server_connect(self): | ||
sni_name=hostname) | ||
|
||
@unittest.skipIf(Py_DEBUG_WIN32, "Avoid mixing debug/release CRT on Windows") | ||
+ @unittest.skipIf(Py_OPENSSL_IS_AWSLC, "AWS-LC doesn't support (FF)DHE") | ||
def test_dh_params(self): | ||
# Check we can get a connection with ephemeral Diffie-Hellman | ||
client_context, server_context, hostname = testing_context() | ||
@@ -4364,14 +4366,14 @@ def test_session_handling(self): | ||
def test_psk(self): | ||
psk = bytes.fromhex('deadbeef') | ||
|
||
- client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||
+ client_context, server_context, _ = testing_context() | ||
+ | ||
client_context.check_hostname = False | ||
client_context.verify_mode = ssl.CERT_NONE | ||
client_context.maximum_version = ssl.TLSVersion.TLSv1_2 | ||
client_context.set_ciphers('PSK') | ||
client_context.set_psk_client_callback(lambda hint: (None, psk)) | ||
|
||
- server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | ||
server_context.maximum_version = ssl.TLSVersion.TLSv1_2 | ||
server_context.set_ciphers('PSK') | ||
server_context.set_psk_server_callback(lambda identity: psk) | ||
@@ -4443,14 +4445,14 @@ def server_callback(identity): | ||
self.assertEqual(identity, client_identity) | ||
return psk | ||
|
||
- client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||
+ client_context, server_context, _ = testing_context() | ||
+ | ||
client_context.check_hostname = False | ||
client_context.verify_mode = ssl.CERT_NONE | ||
client_context.minimum_version = ssl.TLSVersion.TLSv1_3 | ||
client_context.set_ciphers('PSK') | ||
client_context.set_psk_client_callback(client_callback) | ||
|
||
- server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | ||
server_context.minimum_version = ssl.TLSVersion.TLSv1_3 | ||
server_context.set_ciphers('PSK') | ||
server_context.set_psk_server_callback(server_callback, identity_hint) | ||
@@ -4461,7 +4463,10 @@ def server_callback(identity): | ||
s.connect((HOST, server.port)) | ||
|
||
|
||
-@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3") | ||
+@unittest.skipUnless( | ||
+ has_tls_version('TLSv1_3') and not Py_OPENSSL_IS_AWSLC, | ||
+ "Test needs TLS 1.3; AWS-LC doesn't support PHA" | ||
+) | ||
class TestPostHandshakeAuth(unittest.TestCase): | ||
def test_pha_setter(self): | ||
protocols = [ | ||
@@ -4737,6 +4742,31 @@ def test_internal_chain_server(self): | ||
self.assertEqual(res, b'\x02\n') | ||
|
||
|
||
+@unittest.skipUnless(Py_OPENSSL_IS_AWSLC, "Only test this against AWS-LC") | ||
+class TestPostHandshakeAuthAwsLc(unittest.TestCase): | ||
+ def test_pha(self): | ||
+ protocols = [ | ||
+ ssl.PROTOCOL_TLS_SERVER, ssl.PROTOCOL_TLS_CLIENT | ||
+ ] | ||
+ for protocol in protocols: | ||
+ client_ctx, server_ctx, hostname = testing_context() | ||
+ client_ctx.load_cert_chain(SIGNED_CERTFILE) | ||
+ self.assertEqual(client_ctx.post_handshake_auth, None) | ||
+ with self.assertRaises(AttributeError): | ||
+ client_ctx.post_handshake_auth = True | ||
+ with self.assertRaises(AttributeError): | ||
+ server_ctx.post_handshake_auth = True | ||
+ | ||
+ with ThreadedEchoServer(context=server_ctx) as server: | ||
+ with client_ctx.wrap_socket( | ||
+ socket.socket(), | ||
+ server_hostname=hostname | ||
+ ) as ssock: | ||
+ ssock.connect((HOST, server.port)) | ||
+ with self.assertRaises(NotImplementedError): | ||
+ ssock.verify_client_post_handshake() | ||
+ | ||
+ | ||
HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') | ||
requires_keylog = unittest.skipUnless( | ||
HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') | ||
diff --git a/Modules/Setup b/Modules/Setup | ||
index cd1cf24..53bcc4c 100644 | ||
--- a/Modules/Setup | ||
+++ b/Modules/Setup | ||
@@ -208,11 +208,11 @@ PYTHONPATH=$(COREPYTHONPATH) | ||
#_hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) -lcrypto | ||
|
||
# To statically link OpenSSL: | ||
-# _ssl _ssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \ | ||
-# -l:libssl.a -Wl,--exclude-libs,libssl.a \ | ||
-# -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a | ||
-# _hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \ | ||
-# -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a | ||
+_ssl _ssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \ | ||
+ -l:libssl.a -Wl,--exclude-libs,libssl.a \ | ||
+ -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a | ||
+_hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \ | ||
+ -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a | ||
|
||
# The _tkinter module. | ||
# | ||
diff --git a/Modules/_ssl.c b/Modules/_ssl.c | ||
index f7fdbf4..204d501 100644 | ||
--- a/Modules/_ssl.c | ||
+++ b/Modules/_ssl.c | ||
@@ -187,6 +187,11 @@ extern const SSL_METHOD *TLSv1_2_method(void); | ||
#endif | ||
|
||
|
||
+#if defined(OPENSSL_NO_TLS_PHA) || !defined(TLS1_3_VERSION) || defined(OPENSSL_NO_TLS1_3) | ||
+ #define PY_SSL_NO_POST_HS_AUTH | ||
+#endif | ||
+ | ||
+ | ||
enum py_ssl_error { | ||
/* these mirror ssl.h */ | ||
PY_SSL_ERROR_NONE, | ||
@@ -231,7 +236,7 @@ enum py_proto_version { | ||
PY_PROTO_TLSv1 = TLS1_VERSION, | ||
PY_PROTO_TLSv1_1 = TLS1_1_VERSION, | ||
PY_PROTO_TLSv1_2 = TLS1_2_VERSION, | ||
-#ifdef TLS1_3_VERSION | ||
+#if defined(TLS1_3_VERSION) | ||
PY_PROTO_TLSv1_3 = TLS1_3_VERSION, | ||
#else | ||
PY_PROTO_TLSv1_3 = 0x304, | ||
@@ -293,7 +298,7 @@ typedef struct { | ||
*/ | ||
unsigned int hostflags; | ||
int protocol; | ||
-#ifdef TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
int post_handshake_auth; | ||
#endif | ||
PyObject *msg_cb; | ||
@@ -873,7 +878,7 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, | ||
SSL_set_mode(self->ssl, | ||
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_AUTO_RETRY); | ||
|
||
-#ifdef TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
if (sslctx->post_handshake_auth == 1) { | ||
if (socket_type == PY_SSL_SERVER) { | ||
/* bpo-37428: OpenSSL does not ignore SSL_VERIFY_POST_HANDSHAKE. | ||
@@ -1016,6 +1021,7 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self) | ||
} while (err.ssl == SSL_ERROR_WANT_READ || | ||
err.ssl == SSL_ERROR_WANT_WRITE); | ||
Py_XDECREF(sock); | ||
+ | ||
if (ret < 1) | ||
return PySSL_SetError(self, __FILE__, __LINE__); | ||
if (PySSL_ChainExceptions(self) < 0) | ||
@@ -2775,7 +2781,7 @@ static PyObject * | ||
_ssl__SSLSocket_verify_client_post_handshake_impl(PySSLSocket *self) | ||
/*[clinic end generated code: output=532147f3b1341425 input=6bfa874810a3d889]*/ | ||
{ | ||
-#ifdef TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
int err = SSL_verify_client_post_handshake(self->ssl); | ||
if (err == 0) | ||
return _setSSLError(get_state_sock(self), NULL, 0, __FILE__, __LINE__); | ||
@@ -3198,7 +3204,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) | ||
X509_VERIFY_PARAM_set_flags(params, X509_V_FLAG_TRUSTED_FIRST); | ||
X509_VERIFY_PARAM_set_hostflags(params, self->hostflags); | ||
|
||
-#ifdef TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
self->post_handshake_auth = 0; | ||
SSL_CTX_set_post_handshake_auth(self->ctx, self->post_handshake_auth); | ||
#endif | ||
@@ -3576,7 +3582,7 @@ set_maximum_version(PySSLContext *self, PyObject *arg, void *c) | ||
return set_min_max_proto_version(self, arg, 1); | ||
} | ||
|
||
-#ifdef TLS1_3_VERSION | ||
+#if defined(TLS1_3_VERSION) && !defined(OPENSSL_NO_TLS1_3) | ||
static PyObject * | ||
get_num_tickets(PySSLContext *self, void *c) | ||
{ | ||
@@ -3607,7 +3613,7 @@ set_num_tickets(PySSLContext *self, PyObject *arg, void *c) | ||
|
||
PyDoc_STRVAR(PySSLContext_num_tickets_doc, | ||
"Control the number of TLSv1.3 session tickets"); | ||
-#endif /* TLS1_3_VERSION */ | ||
+#endif /* defined(TLS1_3_VERSION) */ | ||
|
||
static PyObject * | ||
get_security_level(PySSLContext *self, void *c) | ||
@@ -3710,14 +3716,14 @@ set_check_hostname(PySSLContext *self, PyObject *arg, void *c) | ||
|
||
static PyObject * | ||
get_post_handshake_auth(PySSLContext *self, void *c) { | ||
-#if TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
return PyBool_FromLong(self->post_handshake_auth); | ||
#else | ||
Py_RETURN_NONE; | ||
#endif | ||
} | ||
|
||
-#if TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
static int | ||
set_post_handshake_auth(PySSLContext *self, PyObject *arg, void *c) { | ||
if (arg == NULL) { | ||
@@ -4959,14 +4965,14 @@ static PyGetSetDef context_getsetlist[] = { | ||
(setter) _PySSLContext_set_msg_callback, NULL}, | ||
{"sni_callback", (getter) get_sni_callback, | ||
(setter) set_sni_callback, PySSLContext_sni_callback_doc}, | ||
-#ifdef TLS1_3_VERSION | ||
+#if defined(TLS1_3_VERSION) && !defined(OPENSSL_NO_TLS1_3) | ||
{"num_tickets", (getter) get_num_tickets, | ||
(setter) set_num_tickets, PySSLContext_num_tickets_doc}, | ||
#endif | ||
{"options", (getter) get_options, | ||
(setter) set_options, NULL}, | ||
{"post_handshake_auth", (getter) get_post_handshake_auth, | ||
-#ifdef TLS1_3_VERSION | ||
+#if !defined(PY_SSL_NO_POST_HS_AUTH) | ||
(setter) set_post_handshake_auth, | ||
#else | ||
NULL, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import datetime | ||
|
||
import awscrt | ||
import awscrt.auth | ||
import awscrt.http | ||
import boto3 | ||
import botocore | ||
|
||
AWS_ACCESS_KEY_ID = "AKIDEXAMPLE" | ||
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" | ||
AWS_SESSION_TOKEN = None | ||
|
||
AWS_REGION = "us-east-1" | ||
SERVICE = "service" | ||
DATE = datetime.datetime( | ||
year=2015, | ||
month=8, | ||
day=30, | ||
hour=12, | ||
minute=36, | ||
second=0, | ||
tzinfo=datetime.timezone.utc, | ||
) | ||
|
||
credentials_provider = awscrt.auth.AwsCredentialsProvider.new_static( | ||
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN | ||
) | ||
|
||
|
||
def test_awscrt_sigv4(): | ||
signing_config = awscrt.auth.AwsSigningConfig( | ||
algorithm=awscrt.auth.AwsSigningAlgorithm.V4, | ||
signature_type=awscrt.auth.AwsSignatureType.HTTP_REQUEST_HEADERS, | ||
credentials_provider=credentials_provider, | ||
region=AWS_REGION, | ||
service=SERVICE, | ||
date=DATE, | ||
) | ||
|
||
http_request = awscrt.http.HttpRequest( | ||
method="GET", | ||
path="/", | ||
headers=awscrt.http.HttpHeaders([("Host", "example.amazonaws.com")]), | ||
) | ||
|
||
signing_result = awscrt.auth.aws_sign_request(http_request, signing_config).result() | ||
assert signing_result is not None | ||
print(f"Authorization: {signing_result.headers.get('Authorization')}") | ||
|
||
|
||
def test_boto3(): | ||
# make an API call to exercise SigV4 request signing in the CRT. the creds are | ||
# nonsense, but that's OK because we sign and make a request over the network | ||
# to determine that. | ||
client = boto3.client( | ||
"s3", | ||
aws_access_key_id=AWS_ACCESS_KEY_ID, | ||
aws_secret_access_key=AWS_SECRET_ACCESS_KEY, | ||
aws_session_token=AWS_SESSION_TOKEN, | ||
) | ||
try: | ||
client.list_buckets() | ||
assert False, "ListBuckets succeeded when it shouldn't have" | ||
except botocore.exceptions.ClientError as e: | ||
# expect it to fail due to nonsense creds | ||
assert "InvalidAccessKeyId" in e.response["Error"]["Code"] | ||
|
||
|
||
if __name__ == "__main__": | ||
# discover test functions defined in __main__'s scope and execute them. | ||
for test in [test + "()" for test in globals() if test.startswith("test_")]: | ||
print(f"running {test}...") | ||
eval(test) | ||
print("done.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import sys | ||
|
||
assert sys.version_info.major == 3, "Only python 3 supported" | ||
if sys.version_info.minor >= 13: | ||
print("Fernet import currently broken on python release candidates >= 3.13") | ||
print("Returning early for now, need to check in on this post-release") | ||
sys.exit() | ||
|
||
import cryptography | ||
import cryptography.hazmat.backends.openssl.backend | ||
from cryptography.fernet import Fernet | ||
|
||
# exercise simple round trip, then assert that PyCA has linked OpenSSL | ||
k = Fernet.generate_key() | ||
f = Fernet(k) | ||
pt = b"hello world" | ||
assert pt == f.decrypt(f.encrypt(pt)) | ||
|
||
version = cryptography.hazmat.backends.openssl.backend.openssl_version_text() | ||
assert "OpenSSL" in version, f"PyCA didn't link OpenSSL: {version}" | ||
assert "AWS-LC" not in version, f"PyCA linked AWS-LC: {version}" |
Oops, something went wrong.