Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow OpenSSL 3.0 as an OpenSSL provider. #5050

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
10 changes: 9 additions & 1 deletion tests/integrationv2/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# SPDX-License-Identifier: Apache-2.0
import os
import pytest
import subprocess
from global_flags import set_flag, S2N_PROVIDER_VERSION, S2N_FIPS_MODE
from providers import S2N, JavaSSL
from providers import S2N, JavaSSL, OpenSSL

PATH_CONFIGURATION_KEY = pytest.StashKey()

Expand All @@ -29,6 +30,13 @@ def available_providers():

if os.path.exists("./bin/SSLSocketClient.class"):
providers.add(JavaSSL)

openssl_available = subprocess.run(
["openssl", "version"]
)

if openssl_available.returncode == 0:
providers.add(OpenSSL)

return providers

Expand Down
68 changes: 58 additions & 10 deletions tests/integrationv2/processes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import time
import os
import select
import selectors
Expand Down Expand Up @@ -96,7 +97,7 @@ def _communicate(self, input_data=None, send_marker_list=None, close_marker=None
This method will read and write data to a subprocess in a non-blocking manner.
The code is heavily based on Popen.communicate. There are a couple differences:

* STDIN is not registered for events until the read_to_send marker is found
* STDIN is not registered for events until the ready_to_send marker is found
* STDIN is only closed after all registered events have been processed (including
pending stdout/stderr events, allowing more data to be stored).
"""
Expand Down Expand Up @@ -144,6 +145,7 @@ def _communicate(self, input_data=None, send_marker_list=None, close_marker=None
if self.proc.stderr and not self.proc.stderr.closed:
selector.register(self.proc.stderr, selectors.EVENT_READ)

# Keep iterating until we unregister stdin, stdout and stderr
while selector.get_map():
timeout = self._remaining_time(endtime)
if timeout is not None and timeout < 0:
Expand All @@ -157,30 +159,50 @@ def _communicate(self, input_data=None, send_marker_list=None, close_marker=None
ready = selector.select(timeout)
self._check_timeout(endtime, orig_timeout, stdout, stderr)

# (Key, events) tuple represents a single I/O operation
for key, events in ready:
# STDIN is only registered to receive events after the send_marker is found.
if key.fileobj is self.proc.stdin:
print(f'{self.name}: stdin available')
chunk = input_view[input_data_offset:
input_data_offset + _PIPE_BUF]

# will run try block and then else (if no exception was found)
try:
input_data_offset += os.write(key.fd, chunk)
print(f'{self.name}: sent')
except BrokenPipeError:
selector.unregister(key.fileobj)
print(
f"########################################################################################\n"
f"############# {self.name}: Unregistering (stdin) BrokenPipeError #############\n"
f"########################################################################################"
)
selector.unregister(self.proc.stdin)
else:
if input_data_offset >= input_data_len:
selector.unregister(key.fileobj)
print(
f"#############################################################################################\n"
f"############# {self.name}: Unregistering (stdin) Input_data_offset >= input_data_len #############\n"
f"#############################################################################################"
)
selector.unregister(self.proc.stdin)
input_data_sent = True
input_data_offset = 0
if send_marker_list:
send_marker = send_marker_list.pop(0)
print(f'{self.name}: next send_marker is {send_marker}')
elif key.fileobj in (self.proc.stdout, self.proc.stderr):
print(f'{self.name}: stdout available')
# 32 KB (32 × 1024 = 32,768 bytes), read 32KB from the file descriptor
data = os.read(key.fd, 32768)
if not data:
print(
f"##################################################################################\n"
f"################ {self.name}: Unregistering (stdout or stderr) No Data ################\n"
f"##################################################################################"
)
selector.unregister(key.fileobj)

data_str = str(data)

# Prepends n - 1 bytes of previously-seen stdout to the chunk we'll be searching
Expand Down Expand Up @@ -226,25 +248,41 @@ def _communicate(self, input_data=None, send_marker_list=None, close_marker=None
if self.wait_for_marker:
print(f'{self.name}: looking for wait_for_marker {self.wait_for_marker} in {data_debug}')
if self.wait_for_marker is not None and self.wait_for_marker in data_str:
print(
f"################################################################################################\n"
f"############## {self.name}: Unregistering (stdout + stderr), found wait_for_marker ##############\n"
f"################################################################################################\n"
)
selector.unregister(self.proc.stdout)
selector.unregister(self.proc.stderr)
return None, None

if kill_marker:
print(f'{self.name}: looking for kill_marker {kill_marker} in {data}')
if kill_marker is not None and kill_marker in data:
print(
f"###########################################################################################\n"
f"############# {self.name}: Unregistering (stdout + stderr), found kill_marker #############\n"
f"###########################################################################################\n"
)
selector.unregister(self.proc.stdout)
selector.unregister(self.proc.stderr)
self.proc.kill()


# if close_marker and close_marker in data_str:
# print(f'{self.name} Found close marker: closing stdin')
# input_data_sent = None
# selector.unregister(self.proc.stdin)

# If we have finished sending all our input, and have received the
# ready-to-send marker, we can close out stdin.
if self.proc.stdin and input_data_sent and not input_data:
print(f'{self.name}: finished sending')
if close_marker:
print(f'{self.name}: looking for close_marker {close_marker} in {data_debug}')
if close_marker is None or (close_marker and close_marker in data_str):
print(f'{self.name}: closing stdin')
print(f'{self.name} Found close marker: closing stdin')
input_data_sent = None
self.proc.stdin.close()

Expand Down Expand Up @@ -389,12 +427,22 @@ def run(self):
finally:
# This data is dumped to stdout so we capture this
# information no matter where a test fails.
print("Command line: {}".format(" ".join(self.cmd_line)))
print("Exit code: {}".format(proc.returncode))
print("Stdout: {}".format(
proc_results[0].decode("utf-8", "backslashreplace")))
print("Stderr: {}".format(
proc_results[1].decode("utf-8", "backslashreplace")))
print("###############################################################")
print(f"####################### {self.cmd_line[0]} #######################")
print("###############################################################")

print(f"Command line:\n\t{' '.join(self.cmd_line)}")
print(f"Exit code:\n\t {proc.returncode}")

print("##########################################################")
print("########################### Stdout #######################")
print("##########################################################")
print(proc_results[0].decode("utf-8", "backslashreplace"))

print("#########################################################")
print("########################### Stderr #######################")
print("#########################################################")
print(proc_results[1].decode("utf-8", "backslashreplace"))

def kill(self):
self.proc.kill()
Expand Down
53 changes: 38 additions & 15 deletions tests/integrationv2/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ def __init__(self, options: ProviderOptions):

self.options = options
if self.options.mode == Provider.ServerMode:
self.cmd_line = self.setup_server() # lgtm [py/init-calls-subclass]
# lgtm [py/init-calls-subclass]
self.cmd_line = self.setup_server()
elif self.options.mode == Provider.ClientMode:
self.cmd_line = self.setup_client() # lgtm [py/init-calls-subclass]
# lgtm [py/init-calls-subclass]
self.cmd_line = self.setup_client()

def setup_client(self):
"""
Expand Down Expand Up @@ -140,7 +142,8 @@ class S2N(Provider):
def __init__(self, options: ProviderOptions):
Provider.__init__(self, options)

self.send_with_newline = True # lgtm [py/overwritten-inherited-attribute]
# lgtm [py/overwritten-inherited-attribute]
self.send_with_newline = True

@classmethod
def get_send_marker(cls):
Expand Down Expand Up @@ -334,12 +337,18 @@ def setup_server(self):


class OpenSSL(Provider):
result = subprocess.run(
["openssl", "version"], shell=False, capture_output=True, text=True
)
version_str = result.stdout.split(" ")
# This will return just the version number
version_openssl = version_str[1]

def __init__(self, options: ProviderOptions):
Provider.__init__(self, options)
# We print some OpenSSL logging that includes stderr
self.expect_stderr = True # lgtm [py/overwritten-inherited-attribute]
# Current provider needs 1.1.x https://github.com/aws/s2n-tls/issues/3963
self._is_openssl_11()
self.at_least_openssl_1_1()

@classmethod
def get_send_marker(cls):
Expand Down Expand Up @@ -388,11 +397,17 @@ def _cipher_to_cmdline(self, cipher):

@classmethod
def get_version(cls):
return get_flag(S2N_PROVIDER_VERSION)
return cls.version_openssl

@classmethod
def supports_protocol(cls, protocol, with_cert=None):
if protocol is Protocols.SSLv3:
if cls.get_version()[0:3] == "1.1" and protocol is Protocols.SSLv3:
return False
if cls.get_version()[0:3] == "3.0" and (
protocol is Protocols.SSLv3
or protocol is Protocols.TLS10
or protocol is Protocols.TLS11
):
return False

return True
Expand All @@ -401,14 +416,18 @@ def supports_protocol(cls, protocol, with_cert=None):
def supports_cipher(cls, cipher, with_curve=None):
return True

def _is_openssl_11(self) -> None:
result = subprocess.run(["openssl", "version"], shell=False, capture_output=True, text=True)
def at_least_openssl_1_1(self) -> None:
result = subprocess.run(["openssl", "version"],
shell=False, capture_output=True, text=True)
version_str = result.stdout.split(" ")
project = version_str[0]
version = version_str[1]

print(f"openssl version: {project} version: {version}")
if (project != "OpenSSL" or version[0:3] != "1.1"):
raise FileNotFoundError(f"Openssl version returned {version}, expected 1.1.x.")
if project != "OpenSSL" or version[0:3] < "1.1":
raise FileNotFoundError(
f"Openssl version returned {version}, expected at least 1.1.x."
)

def setup_client(self):
cmd_line = ['openssl', 's_client']
Expand Down Expand Up @@ -701,7 +720,8 @@ def __init__(self, options: ProviderOptions):
Provider.__init__(self, options)

self.expect_stderr = True # lgtm [py/overwritten-inherited-attribute]
self.send_with_newline = True # lgtm [py/overwritten-inherited-attribute]
# lgtm [py/overwritten-inherited-attribute]
self.send_with_newline = True

@staticmethod
def cipher_to_priority_str(cipher):
Expand Down Expand Up @@ -773,13 +793,15 @@ def get_send_marker(cls):
def create_priority_str(self):
priority_str = "NONE"

protocol_to_priority_str = self.protocol_to_priority_str(self.options.protocol)
protocol_to_priority_str = self.protocol_to_priority_str(
self.options.protocol)
if protocol_to_priority_str:
priority_str += ":+" + protocol_to_priority_str
else:
priority_str += ":+VERS-ALL"

cipher_to_priority_str = self.cipher_to_priority_str(self.options.cipher)
cipher_to_priority_str = self.cipher_to_priority_str(
self.options.cipher)
if cipher_to_priority_str:
priority_str += ":+" + cipher_to_priority_str
else:
Expand All @@ -791,7 +813,8 @@ def create_priority_str(self):
else:
priority_str += ":+GROUP-ALL"

sigalg_to_priority_str = self.sigalg_to_priority_str(self.options.signature_algorithm)
sigalg_to_priority_str = self.sigalg_to_priority_str(
self.options.signature_algorithm)
if sigalg_to_priority_str:
priority_str += ":+" + sigalg_to_priority_str
else:
Expand Down
1 change: 0 additions & 1 deletion tests/integrationv2/test_client_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ def test_client_auth_with_s2n_client_no_cert(managed_process, provider, other_pr
def test_client_auth_with_s2n_client_with_cert(managed_process, provider, other_provider, protocol, cipher, certificate,
client_certificate):
port = next(available_ports)

random_bytes = data_bytes(64)
client_options = ProviderOptions(
mode=Provider.ClientMode,
Expand Down
23 changes: 21 additions & 2 deletions tests/integrationv2/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from common import Protocols
from providers import S2N
from common import Certificates, Protocols
from providers import OpenSSL, S2N
from global_flags import get_flag, S2N_FIPS_MODE


Expand Down Expand Up @@ -37,6 +37,8 @@ def get_expected_openssl_version(protocol):
Protocols.TLS13.value: "TLSv1.3"
}.get(protocol.value)

def get_openssl_version():
return OpenSSL.get_version()

def get_expected_gnutls_version(protocol):
return {
Expand Down Expand Up @@ -83,6 +85,23 @@ def invalid_test_parameters(*args, **kwargs):
if not provider_.supports_protocol(protocol):
return True

# If openSSL is 3.0, we should skip all testcases with certificates/client certificates of 1024
if certificate is not None:
if get_openssl_version()[0:3] == "3.0" and (
certificate is Certificates.RSA_1024_SHA256
or certificate is Certificates.RSA_1024_SHA384
or certificate is Certificates.RSA_1024_SHA384
):
return True

if client_certificate is not None:
if get_openssl_version()[0:3] == "3.0" and (
client_certificate is Certificates.RSA_1024_SHA256
or client_certificate is Certificates.RSA_1024_SHA384
or client_certificate is Certificates.RSA_1024_SHA384
):
return True

if cipher is not None:
# If the selected protocol doesn't allow the cipher, don't test
if protocol is not None:
Expand Down