diff --git a/dbc2val/Readme.md b/dbc2val/Readme.md index fa08348a..f9d89fd1 100644 --- a/dbc2val/Readme.md +++ b/dbc2val/Readme.md @@ -248,6 +248,8 @@ A smaller excerpt from the above sample, with fewer signals. | - | *KUKSA_ADDRESS* | *[general].ip* | `127.0.0.1` | IP address for Server/Databroker | | - | *KUKSA_PORT* | *[general].port* | `55555` | Port for Server/Databroker | | *--tls* | - | *[general].tls* | `False` | Shall tls be used for Server/Databroker connection? | +| | - | *[general].root_ca_path* | *Undefined* | Path to root CA: Only needed if using TLS | +| | - | *[general].tls_server_name* | *Undefined* | TLS server name, may be needed if addressing a server by IP-name | | - | - | *[general].token* | *Undefined* | Token path. Only needed if Databroker/Server requires authentication | | - | *VEHICLEDATABROKER_DAPR_APP_ID* | - | - | Add dapr-app-id metadata. Only relevant for KUKSA.val Databroker | | *--dbc2val /--no-dbc2val* | *USE_DBC2VAL* / *NO_USE_DBC2VAL* | *[can].dbc2val* | dbc2val enabled | Specifies if sending data from CAN to KUKSA.val is enabled. Setting the environment variable to any value is equivalent to activating the switch on the command line.| @@ -271,6 +273,35 @@ Configuration options have the following priority (highest at top). 3. configuration file 4. default value +### Using kuksa-client with a server requiring Authorization + +The [default configuration file](config/dbc-feeder.ini) does not specify any token to use. +If the KUKSA.val Databroker or KUKSA.val Server requires authorization the `token` attribute in the config file +must be set. The default config file include (commented) values to use if using KUKSA.val example tokens. + +*Note: Production deployments are strongly recommend to use Authorization but must NOT use the example tokens available in the KUKSA.val repository!* + +### Using kuksa-client with a server requiring TLS + +The [default configuration file](config/dbc-feeder.ini) does not specify that TLS shall be used. +If the KUKSA.val Databroker or KUKSA.val Server requires authentication the `tls` attribute in the config file +must be set to `True` and `root_ca_path` must be set. +The default config file include (commented) values to use if using KUKSA.val example certificates. + +The feeder verifies that the Databroker/Server presents a certificate with a name matching the server. +The KUKSA.val default server certificate include `Server`, `localhost` and `127.0.0.1` as names, but due to a limitation +name validation does not work when using gRPC and a numeric IP-address, so for that combination you must as a work around +specify the `tls_server_name` to use in name validation, like in the example below. + +``` +ip = 127.0.0.1 +tls = True +root_ca_path=../../kuksa.val/kuksa_certificates/CA.pem +tls_server_name=localhost +``` + +*Note: Production deployments are strongly recommend to use TLS but must NOT use the example certificates available in the KUKSA.val repository!* + ## Using kuksa-val-server 1. To make the feeder communicate with this server, use the `--server-type kuksa_val_server` CLI option or refer to [Configuration](#configuration) for `server-type`. diff --git a/dbc2val/config/dbc_feeder.ini b/dbc2val/config/dbc_feeder.ini index 19bd4887..4f98f18d 100644 --- a/dbc2val/config/dbc_feeder.ini +++ b/dbc2val/config/dbc_feeder.ini @@ -33,12 +33,20 @@ port = 55555 tls = False # tls = True +# TLS-related settings +# Path to root CA, needed if using TLS +root_ca_path=../../kuksa.val/kuksa_certificates/CA.pem +# Server name, typically only needed if accessing server by IP address like 127.0.0.1 +# and typically only if connection to KUKSA.val Databroker +# If using KUKSA.val example certificates the names "Server" or "localhost" can be used. +# tls_server_name=localhost + # Token file for authorization. # Default behavior differ between servers # For KUKSA.val Databroker the KUKSA.val default token not included in packages and containers # If you run your Databroker so it require authentication you must specify token # The example below works if you have cloned kuksa.val in parallel to kuksa.val.feeders -#token=../../kuksa.val/jwt/provide-all.token +# token=../../kuksa.val/jwt/provide-all.token # For KUKSA.val Server the default behavior is to use the token provided as part of kuksa-client # So you only need to specify a different token if you want to use a different token # Possibly like below diff --git a/dbc2val/dbcfeeder.py b/dbc2val/dbcfeeder.py index 22dcaef7..582ab277 100755 --- a/dbc2val/dbcfeeder.py +++ b/dbc2val/dbcfeeder.py @@ -479,6 +479,19 @@ def main(argv): if "tls" in config["general"]: client_wrapper.set_tls(config["general"].getboolean("tls")) + if "root_ca_path" in config["general"]: + path = config['general']['root_ca_path'] + log.info(f"Given root CA path: {path}") + client_wrapper.set_root_ca_path(path) + elif client_wrapper.get_tls(): + # We do not want to rely on kuksa-client default + log.error("Root CA must be given when using TLS") + + if "tls_server_name" in config["general"]: + name = config['general']['tls_server_name'] + log.info(f"Given TLS server name: {name}") + client_wrapper.set_tls_server_name(name) + if "token" in config["general"]: log.info(f"Given token information: {config['general']['token']}") client_wrapper.set_token_path(config["general"]["token"]) diff --git a/dbc2val/dbcfeederlib/clientwrapper.py b/dbc2val/dbcfeederlib/clientwrapper.py index b1012433..362494e9 100644 --- a/dbc2val/dbcfeederlib/clientwrapper.py +++ b/dbc2val/dbcfeederlib/clientwrapper.py @@ -36,6 +36,8 @@ def __init__(self, ip: str, port: int, token_path: str, tls: bool = True): self._token_path = token_path self._tls = tls self._registered = False + self._root_ca_path = None + self._tls_server_name = None def set_ip(self, ip: str): """ Set IP address to use """ @@ -52,6 +54,20 @@ def set_tls(self, tls: bool): """ self._tls = tls + def get_tls(self) -> bool: + """ + Return TLS setting + """ + return self._tls + + def set_root_ca_path(self, path: str): + """ Set Path for Root CA (CA.pem) """ + self._root_ca_path = path + + def set_tls_server_name(self, name: str): + """ Set Path for Root CA (CA.pem) """ + self._tls_server_name = name + def set_token_path(self, token_path: str): self._token_path = token_path diff --git a/dbc2val/dbcfeederlib/databrokerclientwrapper.py b/dbc2val/dbcfeederlib/databrokerclientwrapper.py index dbba9ef2..ffaae012 100644 --- a/dbc2val/dbcfeederlib/databrokerclientwrapper.py +++ b/dbc2val/dbcfeederlib/databrokerclientwrapper.py @@ -21,6 +21,7 @@ import contextlib import grpc.aio +from pathlib import Path import kuksa_client.grpc from kuksa_client.grpc import Datapoint @@ -90,10 +91,19 @@ def start(self): # The alternative approach would be to provide token in constructor # with/without ensure_startup_connection and not actively call "authorize" # The only practical difference is how progress and errors (if any) are reported! + + # If there is a path VSSClient will request a secure connection + if self._tls and self._root_ca_path: + root_path = Path(self._root_ca_path) + else: + root_path = None + self._grpc_client = self._exit_stack.enter_context(kuksa_client.grpc.VSSClient( host=self._ip, port=self._port, - ensure_startup_connection=False + ensure_startup_connection=False, + root_certificates=root_path, + tls_server_name=self._tls_server_name )) self._grpc_client.authorize(token=self._token, **self._rpc_kwargs) self._grpc_client.channel.subscribe( @@ -198,7 +208,15 @@ async def subscribe(self, vss_names: List[str], callback): subscribe_entry = SubscribeEntry(name, View.FIELDS, [Field.ACTUATOR_TARGET]) log.info(f"Subscribe entry: {subscribe_entry}") entries.append(subscribe_entry) - async with VSSClient(self._ip, self._port, token=self._token) as client: + + # If there is a path VSSClient will request a secure connection + if self._tls and self._root_ca_path: + root_path = Path(self._root_ca_path) + else: + root_path = None + + async with VSSClient(self._ip, self._port, token=self._token, + root_certificates=root_path, tls_server_name=self._tls_server_name) as client: async for updates in client.subscribe(entries=entries): log.debug(f"Received update of length {len(updates)}") await callback(updates) diff --git a/dbc2val/dbcfeederlib/serverclientwrapper.py b/dbc2val/dbcfeederlib/serverclientwrapper.py index ee609349..951e4e2c 100644 --- a/dbc2val/dbcfeederlib/serverclientwrapper.py +++ b/dbc2val/dbcfeederlib/serverclientwrapper.py @@ -61,7 +61,10 @@ def start(self): if self._token_path != "": self._client_config["token"] = self._token_path - # TODO add data for root cert if using TLS and if given + if self._root_ca_path: + self._client_config['cacertificate'] = self._root_ca_path + if self._tls_server_name: + self._client_config['tls_server_name'] = self._tls_server_name self._kuksa = KuksaClientThread(self._client_config) self._kuksa.start() diff --git a/dbc2val/requirements.txt b/dbc2val/requirements.txt index 2d1249f1..eb64ca6c 100644 --- a/dbc2val/requirements.txt +++ b/dbc2val/requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile requirements.in +# pip-compile --pre requirements.in # argparse-addons==0.12.0 # via cantools @@ -22,47 +22,45 @@ crccheck==1.3.0 # via cantools decorator==5.1.1 # via jsonpath-ng -diskcache==5.5.1 +diskcache==5.6.1 # via cantools exceptiongroup==1.1.1 # via pytest -grpcio==1.53.0 - # via - # grpcio-tools - # kuksa-client -grpcio-tools==1.53.0 +grpcio==1.56.0rc2 + # via grpcio-tools +grpcio-tools==1.56.0rc2 # via kuksa-client iniconfig==2.0.0 # via pytest jsonpath-ng==1.5.3 # via kuksa-client -kuksa-client==0.4.0a1 +kuksa-client==0.4.0a3 # via -r requirements.in msgpack==1.0.5 # via python-can -numpy==1.24.2 +numpy==1.25.0 # via can-j1939 packaging==23.1 # via # pytest # python-can -pluggy==1.0.0 +pluggy==1.2.0 # via pytest ply==3.11 # via jsonpath-ng -protobuf==4.22.3 +protobuf==4.23.3 # via grpcio-tools py-expression-eval==0.3.14 # via -r requirements.in -pygments==2.15.0 +pygments==2.15.1 # via kuksa-client pyperclip==1.8.2 # via cmd2 pyserial==3.5 # via -r requirements.in -pytest==7.3.1 +pytest==7.3.2 # via can-j1939 -python-can==4.1.0 +python-can==4.2.2 # via # -r requirements.in # can-j1939 @@ -75,17 +73,17 @@ textparser==0.24.0 # via cantools tomli==2.0.1 # via pytest -types-protobuf==4.22.0.2 +types-protobuf==4.23.0.1 # via -r requirements.in -types-pyyaml==6.0.12.9 +types-pyyaml==6.0.12.10 # via -r requirements.in -typing-extensions==4.5.0 +typing-extensions==4.7.0rc1 # via # cantools # python-can wcwidth==0.2.6 # via cmd2 -websockets==11.0.1 +websockets==11.0.3 # via kuksa-client wrapt==1.15.0 # via python-can diff --git a/dds2val/README.md b/dds2val/README.md index 3c52c761..2214e169 100644 --- a/dds2val/README.md +++ b/dds2val/README.md @@ -17,7 +17,7 @@ The DDS provider provides data from an DDS middleware/API. For further understan ### KML replay -*This requires that you already haev created the virtual environment as described in "local build" above* +*This requires that you already have created the virtual environment as described in "local build" above* 1. `source env/bin/activate` 2. `pip install -r requirements/requirements-kml.txt` @@ -38,12 +38,14 @@ Configuration for the DDS provider is solved through setting environment variabl | Environment variable | default value | description | | ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| VEHICLEDATABROKER_DAPR_APP_ID | None | DAPR ID for Vehicle App to look for. For more information to Vehicle Apps visit [Velocitas](https://eclipse-velocitas.github.io/velocitas-docs/) | | VDB_ADDRESS | 127.0.0.1 | Address where to look for (vehicle) databroker | +| VEHICLEDATABROKER_DAPR_APP_ID | None | DAPR ID for Vehicle App to look for. For more information to Vehicle Apps visit [Velocitas](https://eclipse-velocitas.github.io/velocitas-docs/) | | DAPR_GRPC_PORT | None | If [DAPR](https://dapr.io/) gets used port of DAPR Sidecar. Overwrites VDB_PORT variable | | MAPPING_FILE | mapping/latest/mapping.yml | Place of mapping file from DDS to VSS | | VDB_PORT | 55555 | On which port the (vehicle) databroker is expected. If you want to use DAPR use DAPR_GRPC_PORT. | | TOKEN | None | JWT token which will get used to authorize to databroker; You can set on linux through `export TOKEN=$(< PATH_TO_kuksa.val/jwt/provide-all.token)` or `export TOKEN=`| | +| VDB_ROOT_CA_PATH | None | Path to root CA. If defined the client will attempt to use a secure connection and identify the server using this certificate. | +| VDB_TLS_SERVER_NAME | None | TLS server name, may be needed if addressing a server by IP-name | ## Overall sequence diff --git a/dds2val/ddsprovider.py b/dds2val/ddsprovider.py index 3101c8a7..c0d1c067 100755 --- a/dds2val/ddsprovider.py +++ b/dds2val/ddsprovider.py @@ -51,13 +51,25 @@ async def main(): port = os.environ.get("DAPR_GRPC_PORT") else: port = os.environ.get("VDB_PORT", "55555") - databroker_address = os.environ.get("VDB_ADDRESS", "127.0.0.1:") + port + databroker_address = os.environ.get("VDB_ADDRESS", "127.0.0.1") + ":" + port mappingfile = os.environ.get( "MAPPING_FILE", str(Path(__file__).parent / "mapping/latest/mapping.yml") ) - ddsprovider = helper.Ddsprovider() + # Collect data for TLS connection, for now default is no TLS + # To keep backward compatibility not using TLS is default + if os.environ.get("VDB_ROOT_CA_PATH"): + root_ca_path = os.environ.get("VDB_ROOT_CA_PATH") + else: + root_ca_path = None + + if os.environ.get("VDB_TLS_SERVER_NAME"): + tls_server_name = os.environ.get("VDB_TLS_SERVER_NAME") + else: + tls_server_name = None + + ddsprovider = helper.Ddsprovider(root_ca_path, tls_server_name) # Handler for Ctrl-C and Kill signal def signal_handler(signal_received, _frame): diff --git a/dds2val/ddsproviderlib/helper.py b/dds2val/ddsproviderlib/helper.py index 62d29f91..20b6a2ad 100644 --- a/dds2val/ddsproviderlib/helper.py +++ b/dds2val/ddsproviderlib/helper.py @@ -16,6 +16,7 @@ import contextlib import logging import time +from typing import Optional # pylint: disable=unused-import import sensor_msgs # # noqa: F401 @@ -24,6 +25,8 @@ # pylint: disable=unused-import import std_msgs # # noqa: F401 +from pathlib import Path + # F401 Vehicle module is used inside the eval block to access the data classes # pylint: disable=unused-import import Vehicle # noqa: F401 @@ -96,7 +99,7 @@ class Ddsprovider: """class to work with DDS feeder.""" # pylint: disable=too-many-instance-attributes - def __init__(self): + def __init__(self, root_ca_path: Optional[str] = None, tls_server_name: Optional[str] = None): self._shutdown = False self._provider = None self._registered = False @@ -105,6 +108,8 @@ def __init__(self): self._reader = [] self._listener = None self._mapper = None + self._root_ca_path = root_ca_path + self._tls_server_name = tls_server_name self._exit_stack = contextlib.ExitStack() async def start(self, databroker_address, grpc_metadata, mappingfile, token): @@ -118,9 +123,16 @@ async def start(self, databroker_address, grpc_metadata, mappingfile, token): self._mapper = vss2ddsmapper.Vss2DdsMapper(mappingfile) host, port = databroker_address.split(":") + # If there is a path VSSClient will request a secure connection + if self._root_ca_path: + root_path = Path(self._root_ca_path) + else: + root_path = None + try: vss_client = self._exit_stack.enter_context( - VSSClient(host=host, port=port, token=token) + VSSClient(host=host, port=port, token=token, + root_certificates=root_path, tls_server_name=self._tls_server_name) ) except VSSClientError as kuksa_error: log.error(kuksa_error) diff --git a/gps2val/config/gpsd_feeder.ini b/gps2val/config/gpsd_feeder.ini index 9709f65a..12506708 100644 --- a/gps2val/config/gpsd_feeder.ini +++ b/gps2val/config/gpsd_feeder.ini @@ -6,7 +6,6 @@ # insecure = False / True # Paths to files that are needed: for default files see https://github.com/boschglobal/kuksa.val/tree/master/kuksa_certificates # token_or_tokenfile = your token or path to a file storing the token to be used for authentication -# certificate = your path to Client.pem # cacertificate = your path to CA.pem # key = your path to Client.key diff --git a/gps2val/gpsd_feeder.py b/gps2val/gpsd_feeder.py index c254fa8a..c17583b8 100755 --- a/gps2val/gpsd_feeder.py +++ b/gps2val/gpsd_feeder.py @@ -249,7 +249,7 @@ def shutdown(self): logging.getLogger(logger).setLevel(level) manual_config = argparse.ArgumentParser() - manual_config.add_argument("--host", + manual_config.add_argument("--ip", help="Specify the host where too look for KUKSA.val server/databroker; " "default: 127.0.0.1", nargs='?', default="127.0.0.1") @@ -261,16 +261,16 @@ def shutdown(self): "If you want to connect to KUKSA.val databroker specify grpc; default: ws", nargs='?', default="ws") manual_config.add_argument("--insecure", - help="For KUKSA.val server specify False, " - "for KUKSA.val databroker there is currently no security so specify True; " - "default: False", + help="Specify if you want an insecure connection (i.e. without TLS)" + "default: False", nargs='?', default="False") - manual_config.add_argument("--certificate", - help="Specify the path to your Client.pem file; default: Client.pem", - nargs='?', default="Client.pem") manual_config.add_argument("--cacertificate", - help="Specify the path to your CA.pem; default: CA.pem", + help="Specify the path to your CA.pem; default: CA.pem. " + "Needed when not using insecure mode", nargs='?', default="CA.pem") + manual_config.add_argument("--tls-server-name", + help="TLS server name, may be needed if addressing a server by IP-name", + nargs='?', default="") manual_config.add_argument("--token", help="Specify the JWT token string or the path to your JWT token; default: " "authentication information not specified", @@ -293,12 +293,12 @@ def shutdown(self): config_object = configparser.ConfigParser() log.info("No configuration file found. Using default values.") config_object["kuksa_val"] = { - "host": args.host, + "ip": args.ip, "port": args.port, "protocol": args.protocol, "insecure": args.insecure, - "certificate": args.certificate, "cacertificate": args.cacertificate, + "tls_server_name": args.tls_server_name, "token_or_tokenfile": args.token, "file": args.file, } diff --git a/gps2val/readme.md b/gps2val/readme.md index fd749d04..115de2a0 100644 --- a/gps2val/readme.md +++ b/gps2val/readme.md @@ -13,29 +13,32 @@ If you do not have a gps device, you can use your cellphone to forward gps data gpsd -N udp://0.0.0.0:29998 ``` ## Install dependencies and execution -usage: gpsd_feeder.py [-h] [--host [HOST]] [--port [PORT]] [--protocol [PROTOCOL]] [--insecure [INSECURE]] [--certificate [CERTIFICATE]] [--cacertificate [CACERTIFICATE]] [--token [TOKEN]] + +``` +usage: gpsd_feeder.py [-h] [--ip [IP]] [--port [PORT]] [--protocol [PROTOCOL]] [--insecure [INSECURE]] [--cacertificate [CACERTIFICATE]] [--tls-server-name [TLS_SERVER_NAME]] [--token [TOKEN]] [--file [FILE]] [--gpsd_host [GPSD_HOST]] [--gpsd_port [GPSD_PORT]] [--interval [INTERVAL]] options: ->-h, --help show this help message and exit ->--host [HOST] Specify the host where too look for KUKSA.val server/databroker; default: 127.0.0.1 ->--port [PORT] Specify the port where too look for KUKSA.val server/databroker; default: 8090 ->--protocol [PROTOCOL] - If you want to connect to KUKSA.val server specify ws. If you want to connect to KUKSA.val databroker specify grpc; default: ws ->--insecure [INSECURE] - For KUKSA.val server specify False, for KUKSA.val databroker there is currently no security so specify True; default: False ->--certificate [CERTIFICATE] - Specify the path to your Client.pem file; default: Client.pem ->--cacertificate [CACERTIFICATE] - Specify the path to your CA.pem; default: CA.pem ->--token [TOKEN] Specify the JWT token or the path to your JWT token; default: token information not specified ->--file [FILE] Specify the path to your config file; default: not specifed ->--gpsd_host [GPSD_HOST] - Specify the host for gpsd to start on; default: 127.0.0.1 ->--gpsd_port [GPSD_PORT] - Specify the port for gpsd to start on; default: 2948 ->--interval [INTERVAL] - Specify the interval time for feeding gps data; default: 1 + -h, --help show this help message and exit + --ip [IP] Specify the host where too look for KUKSA.val server/databroker; default: 127.0.0.1 + --port [PORT] Specify the port where too look for KUKSA.val server/databroker; default: 8090 + --protocol [PROTOCOL] + If you want to connect to KUKSA.val server specify ws. If you want to connect to KUKSA.val databroker specify grpc; default: ws + --insecure [INSECURE] + Specify if you want an insecure connection (i.e. without TLS)default: False + --cacertificate [CACERTIFICATE] + Specify the path to your CA.pem; default: CA.pem. Needed when not using insecure mode + --tls-server-name [TLS_SERVER_NAME] + TLS server name, may be needed if addressing a server by IP-name + --token [TOKEN] Specify the JWT token string or the path to your JWT token; default: authentication information not specified + --file [FILE] Specify the path to your config file; by default not defined + --gpsd_host [GPSD_HOST] + Specify the host for gpsd to start on; default: 127.0.0.1 + --gpsd_port [GPSD_PORT] + Specify the port for gpsd to start on; default: 2948 + --interval [INTERVAL] + Specify the interval time for feeding gps data; default: 1 +``` A template config file that can be used together with the `--file` option exists in [config/gpsd_feeder.ini](config/gpsd_feeder.ini). Note that if `--file` is specified all other options @@ -52,10 +55,24 @@ gpsd_feeder will try to authenticate itself towards the KUKSA.val Server/Databro Note that the KUKSA.val Databroker by default does not require authentication. An example for authorizing against KUKSA.val Databroker using an example token is shown below. + ``` python gpsd_feeder.py --protocol grpc --port 55555 --insecure true --token /home/user/kuksa.val/jwt/provide-all.token ``` +## TLS + +The KUKSA.val GPS Feeder supports using TLS connections. A TLS connection will be used unless `--insecure=True` +is specified. When using a TLS connection a path to the root certificate used by the Server/Databroker must be given. +The client validates the name of the server against the certificate provided by the Server/Databroker. +If addressing with a numeric IP-address and using grpc as protocol the "real" server name must be given using +`--tls-server-name`. For the [KUKSA.val example certificates](https://github.com/eclipse/kuksa.val/tree/master/kuksa_certificates) +the names `localhost`and `Server`can be used. + +``` +python gpsd_feeder.py --port 55555 --protocol grpc --ip 127.0.0.1 --cacertificate ~/kuksa.val/kuksa_certificates/CA.pem --tls-server-name Server +``` + ### Using docker You can also use `docker` to execute the feeder platform independently. To build a docker image: