Skip to content

Commit

Permalink
Add ssl context container (Draegerwerk#216)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the title above -->
<!--- Link the corresponding issues after you created the pull request
-->

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
- [x] I have updated the [changelog](../CHANGELOG.md) accordingly.
- [x] I have added tests to cover my changes.
  • Loading branch information
maximilianpilz authored Aug 28, 2023
1 parent bf878ca commit 8e9e489
Show file tree
Hide file tree
Showing 20 changed files with 395 additions and 153 deletions.
1 change: 1 addition & 0 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
run: pip install ".[mypy]"

- name: Run mypy
continue-on-error: true # remove when https://github.com/Draegerwerk/sdc11073/issues/156 is done
uses: sasanquaneuf/mypy-github-action@releases/v1
with:
checkName: 'mypy' # NOTE: this needs to be the same as the job name
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
continue-on-error: true # remove when https://github.com/Draegerwerk/sdc11073/issues/156 is done
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@ venv.bak/
dmypy.json

# Pyre type checker
.pyre/
.pyre/

# Intellij IDEA / PyCharm
.idea/
17 changes: 12 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## Fixed
### Added

- `network` module to handle network adapter stuff of the host computer
- `mypy` static code analysis

### Fixed

- possible choosing wrong ipaddress/network interface [#187](https://github.com/Draegerwerk/sdc11073/issues/187)
- added missing SerialNumber to ThisDeviceType
- no creation of operation target states, they should already exist or are not needed if multi state.
Expand All @@ -18,11 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- fixed a bug where the `SdcConsumer` failed to determine the host network adapter if the ip contained in the `device_location` is on a different subnet
- comparison of extensions would fail [#238](https://github.com/Draegerwerk/sdc11073/issues/238)

## Added
- `network` module to handle network adapter stuff of the host computer
- `mypy` static code analysis

## Changed
### Changed

- when creating a `SdcClient` with a `device_location` or `WsDiscovery` containing an ip where no suitable host network adapter could be determined from, an `NetworkAdapterNotFoundError` is raised
- removed `netconn` module
- renamed Device with Provider in order to be more compliant with sdc11073 names:
Expand All @@ -35,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- SdcLocation class reworked; use 'bldng' instead of 'bld', which better matches the standard.
- Some classes renamed in pmtypes.py
- soap client does not try implicit reconnects
- replaced some of the ssl_context parameters with an ssl_context_container parameter,
that can hold two ssl context objects, to be able to get rid of
the deprecation warning occurring when using the same ssl context for both client and server side

## [2.0.0a5] - 2023-06-27

Expand Down
20 changes: 10 additions & 10 deletions examples/ReferenceTest/reference_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from sdc11073 import commlog
from sdc11073 import observableproperties
from sdc11073.certloader import mk_ssl_context_from_folder
from sdc11073.certloader import mk_ssl_contexts_from_folder
from sdc11073.consumer import SdcConsumer
from sdc11073.definitions_sdc import SDC_v1_Definitions
from sdc11073.mdib.consumermdib import ConsumerMdib
Expand Down Expand Up @@ -52,17 +52,17 @@ def run_ref_test():
print('Test step 2: connect to device...')
try:
if ca_folder:
ssl_context = mk_ssl_context_from_folder(ca_folder,
cyphers_file=None,
private_key='user_private_key_encrypted.pem',
certificate='user_certificate_root_signed.pem',
ca_public_key='root_certificate.pem',
ssl_passwd=ssl_passwd,
)
ssl_context_container = mk_ssl_contexts_from_folder(ca_folder,
cyphers_file=None,
private_key='user_private_key_encrypted.pem',
certificate='user_certificate_root_signed.pem',
ca_public_key='root_certificate.pem',
ssl_passwd=ssl_passwd,
)
else:
ssl_context = None
ssl_context_container = None
client = SdcConsumer.from_wsd_service(my_service,
ssl_context=ssl_context,
ssl_context_container=ssl_context_container,
validate=True)
client.start_all()
print('Test step 2 successful: connected to device')
Expand Down
26 changes: 14 additions & 12 deletions examples/ReferenceTest/reference_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
import uuid
from typing import TYPE_CHECKING

import sdc11073.certloader
from sdc11073 import location, network, provider, wsdiscovery
from sdc11073.certloader import mk_ssl_context_from_folder
from sdc11073.certloader import mk_ssl_contexts_from_folder
from sdc11073.loghelper import LoggerAdapter
from sdc11073.mdib import ProviderMdib
from sdc11073.provider.components import SdcProviderComponents
Expand All @@ -22,7 +23,7 @@
from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType

if TYPE_CHECKING:
import ssl
pass

USE_REFERENCE_PARAMETERS = True

Expand All @@ -42,16 +43,16 @@ def get_location() -> location.SdcLocation:
bed=os.getenv('ref_bed', default='r_bed')) # noqa: SIM112


def get_ssl_context() -> ssl.SSLContext | None:
def get_ssl_context() -> sdc11073.certloader.SSLContextContainer | None:
"""Get ssl context from environment or None."""
if (ca_folder := os.getenv('ref_ca')) is None: # noqa: SIM112
return None
return mk_ssl_context_from_folder(ca_folder,
private_key='user_private_key_encrypted.pem',
certificate='user_certificate_root_signed.pem',
ca_public_key='root_certificate.pem',
cyphers_file=None,
ssl_passwd=os.getenv('ref_ssl_passwd')) # noqa: SIM112
return mk_ssl_contexts_from_folder(ca_folder,
private_key='user_private_key_encrypted.pem',
certificate='user_certificate_root_signed.pem',
ca_public_key='root_certificate.pem',
cyphers_file=None,
ssl_passwd=os.getenv('ref_ssl_passwd')) # noqa: SIM112


def get_epr() -> uuid.UUID:
Expand All @@ -68,7 +69,7 @@ def create_reference_provider(
dpws_device: dpws_types.ThisDeviceType | None = None,
epr: uuid.UUID | None = None,
specific_components: SdcProviderComponents | None = None,
ssl_context: ssl.SSLContext | None = None) -> provider.SdcProvider:
ssl_context_container: sdc11073.certloader.SSLContextContainer | None = None) -> provider.SdcProvider:
# generic way to create a device, this what you usually do:
ws_discovery = ws_discovery or wsdiscovery.WSDiscovery(get_network_adapter().ip)
ws_discovery.start()
Expand All @@ -89,7 +90,7 @@ def create_reference_provider(
device_mdib_container=mdib,
epr=epr or get_epr(),
specific_components=specific_components,
ssl_context=ssl_context or get_ssl_context(),
ssl_context_container=ssl_context_container or get_ssl_context(),
)
for desc in prov.mdib.descriptions.objects:
desc.SafetyClassification = pm_types.SafetyClassification.MED_A
Expand Down Expand Up @@ -151,7 +152,8 @@ def run_provider():
else:
specific_components = None # provComponents(services_factory=mk_all_services_except_localization)

prov = create_reference_provider(specific_components=specific_components)
prov = create_reference_provider(ws_discovery=wsd, specific_components=specific_components)
set_reference_data(prov, get_location())

metric = prov.mdib.descriptions.handle.get_one('numeric.ch1.vmd0')
alert_condition = prov.mdib.descriptions.handle.get_one('ac0.mds0')
Expand Down
2 changes: 1 addition & 1 deletion examples/ReferenceTest/test_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _runtest_client_connects(self):
print('Test step 1 successful: device discovered')

print('Test step 2: connect to device...')
client = SdcConsumer.from_wsd_service(my_service, ssl_context=None)
client = SdcConsumer.from_wsd_service(my_service, ssl_context_container=None)
self.my_clients.append(client)
client.start_all()
self.assertTrue(client.is_connected)
Expand Down
62 changes: 40 additions & 22 deletions src/sdc11073/certloader.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import dataclasses
import os
import ssl


def mk_ssl_context_from_folder(ca_folder,
private_key='userkey.pem',
certificate='usercert.pem',
ca_public_key='cacert.pem',
cyphers_file=None,
ssl_passwd=None) -> ssl.SSLContext:
@dataclasses.dataclass
class SSLContextContainer:
client_context: ssl.SSLContext
server_context: ssl.SSLContext


def mk_ssl_contexts_from_folder(ca_folder,
private_key='userkey.pem',
certificate='usercert.pem',
ca_public_key='cacert.pem',
cyphers_file=None,
ssl_passwd=None) -> SSLContextContainer:
"""Convenience method for easy creation of SSL context, assuming all needed files are in the same folder.
Create an ssl context from files 'userkey.pem', 'usercert.pem', and optional 'cacert.pem' and cyphers file
:param ca_folder: base path of all files
:param private_key: name of the private key file of the user
:param certificate: name of the signed certificate of the user
:param ca_public_key: name of public key of the certificate authority that signed the certificate; if given,
verify_mode of ssl_context will be set to CERT_REQUIRED
verify_mode of ssl contexts in the return value will be set to CERT_REQUIRED
:param cyphers_file: optional file that contains a cyphers string; comments are possible, start line with '#'
:param ssl_passwd: optional password string
:return: SSLContext instance
:return: container of SSLContext instances i.e. client_ssl_context and server_ssl_context
"""
certfile = os.path.join(ca_folder, certificate)
if not os.path.exists(certfile):
Expand Down Expand Up @@ -49,29 +56,40 @@ def mk_ssl_context_from_folder(ca_folder,
break
else:
cyphers = None
return mk_ssl_context(keyfile, certfile, cafile, cyphers, ssl_passwd)
return mk_ssl_contexts(keyfile, certfile, cafile, cyphers, ssl_passwd)


def mk_ssl_context(key_file,
cert_file,
ca_file,
cyphers=None,
ssl_passwd=None) -> ssl.SSLContext:
def mk_ssl_contexts(key_file,
cert_file,
ca_file,
cyphers=None,
ssl_passwd=None) -> SSLContextContainer:
"""Convenience method for easy creation of SSL context.
Create an ssl context from files 'userkey.pem', 'usercert.pem', 'cacert.pem' and optional 'cyphers.json'
:param key_file: the private key pem file of the user
:param cert_file: the signed certificate of the user
:param ca_file: optional public key of the certificate authority that signed the certificate; if given,
verify_mode of ssl_context will be set to CERT_REQUIRED
verify_mode of ssl contexts in the return value will be set to CERT_REQUIRED
:param cyphers: optional cyphers string
:param ssl_passwd: optional password string
:return: SSLContext instance
:return: container of SSLContext instances i.e. client_ssl_context and server_ssl_context
"""
ssl_context = ssl.SSLContext() # defaults to ssl.PROTOCOL_TLS
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file, password=ssl_passwd)
client_ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
server_ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

client_ssl_context.check_hostname = False
client_ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file, password=ssl_passwd)
if cyphers is not None:
ssl_context.set_ciphers(cyphers)
client_ssl_context.set_ciphers(cyphers)
if ca_file:
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.load_verify_locations(ca_file)
return ssl_context
client_ssl_context.verify_mode = ssl.CERT_REQUIRED
client_ssl_context.load_verify_locations(ca_file)

server_ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file, password=ssl_passwd)
if cyphers is not None:
server_ssl_context.set_ciphers(cyphers)
if ca_file:
server_ssl_context.verify_mode = ssl.CERT_REQUIRED
server_ssl_context.load_verify_locations(ca_file)

return SSLContextContainer(client_context=client_ssl_context, server_context=server_ssl_context)
Loading

0 comments on commit 8e9e489

Please sign in to comment.