Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/nidmm/system_tests/grpc_server_config_tls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"address": "[::]",
"port": 31762,
"security": "ni-tls-config",
"feature_toggles": {
"ni-tls-config": true
}
}
181 changes: 175 additions & 6 deletions src/nidmm/system_tests/test_system_nidmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import grpc
import hightime
import nitlsconfig
import numpy
import pytest

Expand Down Expand Up @@ -312,7 +313,8 @@ def test_multi_threading_ivi_synchronized_wrapper_releases_lock(self, session):

class TestLibrary(SystemTests):
@pytest.fixture(scope='class')
def session_creation_kwargs(self):
@classmethod
def session_creation_kwargs(cls):
return {}

def test_fetch_waveform_into(self, session):
Expand All @@ -327,17 +329,29 @@ def test_fetch_waveform_into(self, session):
assert not math.isnan(sample)


class TestGrpc(SystemTests):
class TestGrpcSecuredTLS(SystemTests):
@pytest.fixture(scope='class')
def grpc_channel(self):
@classmethod
def grpc_channel(cls):
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)
system_test_utilities.exchange_certificates("localhost")

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config.json')
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = grpc.insecure_channel(f"localhost:{proc.server_port}")
channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
yield channel

@pytest.fixture(scope='class')
def session_creation_kwargs(self, grpc_channel):
@classmethod
def session_creation_kwargs(cls, grpc_channel):
grpc_options = nidmm.GrpcSessionOptions(grpc_channel, '')
return {'grpc_options': grpc_options}

Expand Down Expand Up @@ -369,3 +383,158 @@ def test_attach_to_non_existent_session(self, grpc_channel):
assert e.rpc_code == expected_grpc_error
assert e.description == expected_error_message
assert str(e) == f'{expected_grpc_error}: {expected_error_message}'


class TestGrpcUnsecuredTLS:
@pytest.fixture(scope='function')
def session(self, session_creation_kwargs):
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', **session_creation_kwargs) as simulated_session:
yield simulated_session

@pytest.fixture(scope='class')
@classmethod
def grpc_channel(cls):
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="Disabled",
server_client_mode="Disabled",
client_cert_mode="Disabled",
client_server_mode="Disabled"
)

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
yield channel

@pytest.fixture(scope='class')
@classmethod
def session_creation_kwargs(cls, grpc_channel):
grpc_options = nidmm.GrpcSessionOptions(grpc_channel, '')
return {'grpc_options': grpc_options}

def test_take_simple_measurement_works(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
assert session.read() != 0 # Assumes DMM reading is not exactly zero to support non-connected modules and simulated modules.

def test_acquisition(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
with session.initiate():
session.fetch()
with session.initiate():
session.fetch()

def test_multi_point_acquisition(self, session):
session.configure_multi_point(4, 2)
session.configure_measurement_digits(nidmm.Function.DC_VOLTS, 1, 5.5)
measurements = session.read_multi_point(8)
assert len(measurements) == 8


class TestGrpcNoTLS:
@pytest.fixture(scope='function')
def session(self, session_creation_kwargs):
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', **session_creation_kwargs) as simulated_session:
yield simulated_session

@pytest.fixture(scope='class')
@classmethod
def grpc_channel(cls):
current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_no_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = grpc.insecure_channel(f"localhost:{proc.server_port}")
yield channel

@pytest.fixture(scope='class')
@classmethod
def session_creation_kwargs(cls, grpc_channel):
grpc_options = nidmm.GrpcSessionOptions(grpc_channel, '')
return {'grpc_options': grpc_options}

def test_take_simple_measurement_works(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
assert session.read() != 0 # Assumes DMM reading is not exactly zero to support non-connected modules and simulated modules.

def test_acquisition(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
with session.initiate():
session.fetch()
with session.initiate():
session.fetch()

def test_multi_point_acquisition(self, session):
session.configure_multi_point(4, 2)
session.configure_measurement_digits(nidmm.Function.DC_VOLTS, 1, 5.5)
measurements = session.read_multi_point(8)
assert len(measurements) == 8


def test_unsecured_client():
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)
system_test_utilities.exchange_certificates("localhost")

system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Disabled",
client_server_mode="Disabled"
)

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')

# Attempt to connect to the server. Since it is expecting a TLS-enabled client, this should fail.
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
unsecured_client_channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
grpc_options = nidmm.GrpcSessionOptions(unsecured_client_channel, '')
try:
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', grpc_options=grpc_options):
assert False
except nidmm.Error:
pass


def test_unsecured_server():
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)
system_test_utilities.exchange_certificates("localhost")

system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="Disabled",
server_client_mode="Disabled",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')

# Attempt to connect to the server. Since the client is expecting a TLS-enabled server, this should fail.
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
unsecured_server_channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
grpc_options = nidmm.GrpcSessionOptions(unsecured_server_channel, '')
try:
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', grpc_options=grpc_options):
assert False
except nidmm.Error:
pass
32 changes: 32 additions & 0 deletions src/shared/nitlsconfig_32_bit_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import ctypes
import os
import subprocess


def _patch_subprocess_for_32_bit_nitlsconfig_lookup():
# Because nitlsconfig lives in System32, and the 32-bit system tests are run on a 64-bit machine, the installation
# of nitlsconfig is invisible by default. To get around this, we can disable Wow64 redirection. In order to minimize
# the impact of this, we patch the subprocess initialization specifically for calls to nitlsconfig
if os.name != "nt":
return

original_init = subprocess.Popen.__init__

def patched_init(self, args, *posargs, **kwargs):
command = args[0] if isinstance(args, (list, tuple)) else args
is_nitlsconfig = isinstance(command, str) and os.path.splitext(os.path.basename(command))[0].lower() == "nitlsconfig"
if not is_nitlsconfig:
return original_init(self, args, *posargs, **kwargs)

old = ctypes.c_void_p()
disabled = bool(ctypes.windll.kernel32.Wow64DisableWow64FsRedirection(ctypes.byref(old)))
try:
return original_init(self, args, *posargs, **kwargs)
finally:
if disabled:
ctypes.windll.kernel32.Wow64RevertWow64FsRedirection(old)

subprocess.Popen.__init__ = patched_init


_patch_subprocess_for_32_bit_nitlsconfig_lookup()
122 changes: 122 additions & 0 deletions src/shared/system_test_utilities.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import json
import os
import pathlib
import pytest
import re
import subprocess
import sys
import threading
import time

import nitlsconfig_32_bit_patch # noqa: F401


class GrpcServerProcess:
def __init__(self, config_file_path):
Expand Down Expand Up @@ -104,3 +108,121 @@ def impl_test_multi_threading_ivi_synchronized_wrapper_releases_lock(ivi_method_
t2.start()
t2.join()
assert not t2.is_alive()


def exchange_certificates(
server_host: str,
server_user: str | None = None,
client_host: str | None = None,
client_user: str | None = None,
verbosity: int = 2,
):
# gRPC tests only run on Windows, so this isn't necessary on Linux.
if os.name != "nt":
return

# 26.5 versions of ni-grpc-device server installers do not properly create the trusted.d directory,
# which causes issues with the certificate exchange process. This has been fixed in the 26.8 version
# of the installer, but it has not yet been released. For now, we're creating it manually; this can
# be removed once nimibot system tests are updated to test against >= 26.8 versions of the drivers.
trusted_servers_path = pathlib.Path(r"C:/ProgramData/National Instruments/nitlsconfig/server.d/ni-grpc-device/trusted.d")
trusted_servers_path.mkdir(parents=True, exist_ok=True)

# 26.5 versions of ni-grpc-device client configuration use a default certificate_mode of Disabled,
# which prevents client-side certificate generation from this script. In 26.8 and beyond, the default
# is Managed. We set it manually here; this can be removed once nimibot system tests are updated to
# test against >= 26.8 versions of the drivers.
client_config_path = (
pathlib.Path(os.environ["LOCALAPPDATA"])
/ "National Instruments" / "nitlsconfig" / "client.d" / "ni-grpc-device.conf.yml"
)
content = client_config_path.read_text()
content = re.sub(r"(?m)^certificate_mode:.*$", "certificate_mode: Managed", content)
client_config_path.write_text(content)

script_path = r"C:/NITests/nitlsconfigtest/exchange_certificates.py"
if not pathlib.Path(script_path).is_file():
raise FileNotFoundError(f"Certificate exchange script not found: {script_path}")

server_host_arg = f"--server-host={server_host}"
server_user_arg = f"--server-user={server_user}" if server_user else "--local-server"
client_host_arg = f"--client-host={client_host}" if client_host else None
client_user_arg = f"--client-user={client_user}" if client_user else None

verbosity = max(0, min(verbosity, 4))
verbosity_arg = {
0: "-qq",
1: "-q",
3: "-v",
4: "-vv",
}.get(verbosity)

command = [sys.executable, str(pathlib.Path(script_path)), server_host_arg, server_user_arg]
command.extend(arg for arg in (client_host_arg, client_user_arg, verbosity_arg) if arg is not None)

# The script expects this environment variable to be set
env = os.environ.copy()
env.setdefault("USERNAME", "Administrator")

_run_nitlsconfigtest_script_with_patch(script_path, command[2:], env)


def configure_tls_modes(
service: str,
server_host: str,
server_user: str | None = None,
client_host: str | None = None,
client_user: str | None = None,
server_cert_mode: str | None = None,
server_client_mode: str | None = None,
client_cert_mode: str | None = None,
client_server_mode: str | None = None,
):
# gRPC tests only run on Windows, so this isn't necessary on Linux.
if os.name != "nt":
return

script_path = r"C:/NITests/nitlsconfigtest/configure_tls_modes.py"
if not pathlib.Path(script_path).is_file():
raise FileNotFoundError(f"Configure TLS modes script not found: {script_path}")

service_arg = f"--service={service}"
server_host_arg = f"--server-host={server_host}"
server_user_arg = f"--server-user={server_user}" if server_user else "--local-server"
client_host_arg = f"--client-host={client_host}" if client_host else None
client_user_arg = f"--client-user={client_user}" if client_user else None
server_cert_mode_arg = f"--server-certificate-mode={server_cert_mode}" if server_cert_mode else None
server_client_mode_arg = f"--server-client-mode={server_client_mode}" if server_client_mode else None
client_cert_mode_arg = f"--client-certificate-mode={client_cert_mode}" if client_cert_mode else None
client_server_mode_arg = f"--client-server-mode={client_server_mode}" if client_server_mode else None

command = [sys.executable, str(pathlib.Path(script_path)), service_arg, server_host_arg, server_user_arg]
command.extend(
arg
for arg in (
client_host_arg,
client_user_arg,
server_cert_mode_arg,
server_client_mode_arg,
client_cert_mode_arg,
client_server_mode_arg,
)
if arg is not None
)

# The script expects this environment variable to be set
env = os.environ.copy()
env.setdefault("USERNAME", "Administrator")

_run_nitlsconfigtest_script_with_patch(script_path, command[2:], env)

def _run_nitlsconfigtest_script_with_patch(script_path: str, args: list, env: dict) -> None:
# A bootstrap script is used to import the patcher so that the scripts can see the nitlsconfig executable even if
# they are in a 32-bit context.
bootstrap = (
"import runpy, sys\n"
"import nitlsconfig_32_bit_patch\n"
f"sys.argv = [{script_path!r}] + {args!r}\n"
f"runpy.run_path({script_path!r}, run_name='__main__')\n"
)
subprocess.run([sys.executable, "-c", bootstrap], check=True, env=env)
Loading