Skip to content

Commit

Permalink
TLS support in KUKSA.val Client
Browse files Browse the repository at this point in the history
Update tonic
Make --insecure (or tls config) mandatory for databroker
Remove server hack
  • Loading branch information
erikbosch authored and lukasmittag committed Jun 28, 2023
1 parent 4218c86 commit e96edd4
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 45 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/kuksa-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ on:
- "kuksa-client/**"
workflow_dispatch:


jobs:
checkrights:
uses: ./.github/workflows/check_push_rights.yml
Expand Down Expand Up @@ -95,3 +94,28 @@ jobs:
push: false
tags: "ttl.sh/kuksa.val/kuksa-client-${{github.sha}}:1h"
labels: ${{ steps.meta.outputs.labels }}


dbc2val-test:
runs-on: ubuntu-latest
steps:
- name: Checkout kuksa.val
uses: actions/checkout@v3
- name: Install pip
run: |
python -m pip --quiet --no-input install --upgrade pip
- name: Install dependencies with pip
run: |
cd kuksa-client
pip install -r requirements.txt -e .
pip install -r test-requirements.txt
- name: Run tests
run: |
cd kuksa-client
pytest
- name: Build pypi package
run: |
cd kuksa-client
pip install --upgrade build
python -m build
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ KUKSA.val contains several components
| [Examples](./kuksa_apps) | Multiple example apps for different programming languages and frameworks
| [Feeders](https://github.com/eclipse/kuksa.val.feeders/) | Multiple feeders to gathering vehicle data and transforming it to VSS suitable for kuksa-val-server

## More information

* [KUKSA.val TLS Concept](doc/tls.md)

77 changes: 72 additions & 5 deletions kuksa-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ With default CLI arguments, the client will try to connect to a local VISS serve
If you wish to connect to a gRPC server e.g. `kuksa-databroker`, you should instead run:

```console
$ kuksa-client --ip 127.0.0.1 --port 55555 --protocol grpc --insecure
$ kuksa-client --ip 127.0.0.1 --port 55555 --protocol grpc
```
Note: `--insecure` is required because `kuksa-databroker` does not yet support TLS encryption or authentication.

If everything works as expected and the server can be contacted you will get an output similar to below.

Expand Down Expand Up @@ -64,6 +63,55 @@ Test Client>
If the connected KUKSA.val Server or KUKSA.val Databroker require authorization the next step is to authorize.
KUKSA.val Server and KUKSA.val Databroker use different token formats.

### Connecting to KUKSA.val Databroker

A gRPC connection to KUKSA.val Databroker is started by specifying address and port for the Databroker and giving
`--protocol grpc` as argument.
KUKSA.val Client use TLS by default, it only run in insecure mode if `--insecure` is given as argument.
By default the KUKSA.val example Root CA and Client keys are used, but client keys have no effect as mutual authentication is not supported by KUKSA.val Databroker or KUKSA.val Server.

```
~/kuksa.val/kuksa-client$ kuksa-client --ip localhost --port 55555 --protocol grpc
```

This call with all parameters specified give same effect:

```
~/kuksa.val/kuksa-client$ kuksa-client --ip localhost --port 55555 --protocol grpc --certificate ../kuksa_certificates/Client.pem --keyfile ../kuksa_certificates/Client.key --cacertificate ./kuksa_certificates/CA.pem
```

There is actually no reason to specify client key and certificate, as mutual authentication is not supported in KUKSA.val Databroker,
so the command can be simplified like this:


```
~/kuksa.val/kuksa-client$ kuksa-client --ip localhost --port 55555 --protocol grpc --cacertificate ./kuksa_certificates/CA.pem
```

The example server protocol list 127.0.0.1 as an alternative name, but the TLS-client currently used does not accept it,
instead a valid server name must be given as argument.
Currently `Server` and `localhost`are valid names from the example certificates.

```
~/kuksa.val/kuksa-client$ kuksa-client --ip 127.0.0.1 --port 55555 --protocol grpc --cacertificate ../kuksa_certificates/CA.pem --tls-server-name Server
```

### Connecting to KUKSA.val Server

Connecting to KUKSA.val Server is default, and TLS is used by default by KUKSA.val Server.
`--tls-server-name` does not need to be used when connecting to KUKSA.val Server,
that is the only difference compared to connecting to KUKSA.val Databroker.

```
~/kuksa.val/kuksa-client$ kuksa-client
```

This corresponds to this call:

```
kuksa-client --ip 127.0.0.1 --port 8090 --protocol ws --cacertificate ./kuksa_certificates/CA.pem
```

### Authorizing against KUKSA.val Server

The jwt tokens for testing can either be found under [../kuksa_certificates/jwt](../kuksa_certificates/jwt)
Expand All @@ -82,13 +130,12 @@ Test Client> authorize /some/path/kuksa_certificates/jwt/super-admin.json.token

If connecting to Databroker the command `printTokenDir` is not much help as it shows the default token directories
for KUKSA.val Server example tokens. If the KUKSA.val Databroker use default example tokens then one of the
tokens in [../jwt](..//jwt) can be used, like in the example below:
tokens in [../jwt](../jwt) can be used, like in the example below:

```console
Test Client> authorize /some/path//jwt/provide-all.token
Test Client> authorize /some/path/jwt/provide-all.token
```


## Usage Instructions

Refer help for further information
Expand Down Expand Up @@ -251,6 +298,26 @@ This package holds different APIs depending on your application's requirements.
For more information, see ([Documentation](https://github.com/eclipse/kuksa.val/blob/master/kuksa-client/docs/main.md)).


### TLS configuration

Clients like [KUKSA.val CAN Feeder](https://github.com/eclipse/kuksa.val.feeders/tree/main/dbc2val)
that use KUKSA.val Client library must typically set the path to the root CA certificate.
If the path is set the VSSClient will try to establish a secure connection.

```
# Shall TLS be used (default False for Databroker, True for KUKSA.val Server)
# 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=Server
```

## Troubleshooting

1. The server/data broker is listening on its port but my client is unable to connect to it and returns an error:
Expand Down
55 changes: 50 additions & 5 deletions kuksa-client/kuksa_client/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import functools
import json
import logging.config
import logging
import os
import pathlib
import sys
Expand Down Expand Up @@ -48,6 +49,9 @@
scriptDir = os.path.dirname(os.path.realpath(__file__))


logger = logging.getLogger(__name__)


def assignment_statement(arg):
path, value = arg.split('=', maxsplit=1)
return (path, value)
Expand Down Expand Up @@ -247,8 +251,10 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx):
ap_updateVSSTree.add_argument(
"Json", help="Json tree to update VSS", completer_method=jsonfile_completer_method)

# Constructor
def __init__(self, server_ip=None, server_port=None, server_protocol=None, insecure=False, token_or_tokenfile=None):
# Constructor, request names after protocol to avoid errors
def __init__(self, server_ip=None, server_port=None, server_protocol=None, *, insecure=False, token_or_tokenfile=None,
certificate=None, keyfile=None,
cacertificate=None, tls_server_name=None):
super().__init__(
persistent_history_file=".vssclient_history", persistent_history_length=100, allow_cli_args=False,
)
Expand All @@ -265,6 +271,10 @@ def __init__(self, server_ip=None, server_port=None, server_protocol=None, insec
self.commThread = None
self.token_or_tokenfile = token_or_tokenfile
self.insecure = insecure
self.certificate = certificate
self.keyfile = keyfile
self.cacertificate = cacertificate
self.tls_server_name = tls_server_name

print("Welcome to Kuksa Client version " + str(_metadata.__version__))
print()
Expand Down Expand Up @@ -485,9 +495,21 @@ def connect(self):
config = {'ip': self.serverIP,
'port': self.serverPort,
'insecure': self.insecure,
'protocol': self.serverProtocol,
'token_or_tokenfile': self.token_or_tokenfile,
'protocol': self.serverProtocol
}

# Configs should only be added if they actually have a value
if self.token_or_tokenfile:
config['token_or_tokenfile'] = self.token_or_tokenfile
if self.certificate:
config['certificate'] = self.certificate
if self.keyfile:
config['keyfile'] = self.keyfile
if self.cacertificate:
config['cacertificate'] = self.cacertificate
if self.tls_server_name:
config['tls_server_name'] = self.tls_server_name

self.commThread = KuksaClientThread(config)
self.commThread.start()

Expand Down Expand Up @@ -589,11 +611,34 @@ def main():
parser.add_argument(
'--token_or_tokenfile', default=None, help="JWT token or path to a JWT token file (.token)",
)

# Add TLS arguments
# Note: Databroker does not yet support mutual authentication, so no need to use two first arguments
parser.add_argument(
'--certificate', default=None, help="Client cert file(.pem), only needed for mutual authentication",
)
parser.add_argument(
'--keyfile', default=None, help="Client private key file (.key), only needed for mutual authentication",
)
parser.add_argument(
'--cacertificate', default=None, help="Client root cert file (.pem), needed unless insecure mode used",
)
# Observations for Python
# Connecting to "localhost" works well, subjectAltName seems to suffice
# Connecting to "127.0.0.1" does not work unless server-name specified
# For KUKSA.val example certs default name is "Server"
parser.add_argument(
'--tls-server-name', default=None, help="CA name of server, needed in some cases where subjectAltName does not suffice",
)

args = parser.parse_args()

logging.config.fileConfig(args.logging_config)

clientApp = TestClient(args.ip, args.port, args.protocol,
args.insecure, args.token_or_tokenfile)
insecure = args.insecure, token_or_tokenfile = args.token_or_tokenfile,
certificate = args.certificate, keyfile = args.keyfile,
cacertificate = args.cacertificate, tls_server_name = args.tls_server_name)
try:
# We exit the loop when the user types "quit" or hits Ctrl-D.
clientApp.cmdloop()
Expand Down
4 changes: 3 additions & 1 deletion kuksa-client/kuksa_client/cli_backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import kuksa_certificates



class Backend:
def __init__(self, config):
self.serverIP = config.get('ip', "127.0.0.1")
Expand All @@ -34,8 +35,9 @@ def __init__(self, config):
'cacertificate', str(self.default_cert_path / 'CA.pem'))
self.certificate = config.get('certificate', str(
self.default_cert_path / 'Client.pem'))
self.keyfile = config.get('key', str(
self.keyfile = config.get('keyfile', str(
self.default_cert_path / 'Client.key'))
self.tls_server_name = config.get('tls_server_name', "")
self.token_or_tokenfile = config.get('token_or_tokenfile', str(
self.default_cert_path / 'jwt/all-read-write.json.token'))

Expand Down
1 change: 1 addition & 0 deletions kuksa-client/kuksa_client/cli_backend/grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ async def mainLoop(self):
root_certificates=self.cacertificate,
private_key=self.keyfile,
certificate_chain=self.certificate,
tls_server_name=self.tls_server_name,
token=self.token
) as vss_client:
print("Secure gRPC channel connected.")
Expand Down
17 changes: 12 additions & 5 deletions kuksa-client/kuksa_client/cli_backend/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

from kuksa_client import cli_backend


class Backend(cli_backend.Backend):
def __init__(self, config):
super().__init__(config)
Expand Down Expand Up @@ -235,12 +234,20 @@ async def connect(self):
context.load_cert_chain(
certfile=self.certificate, keyfile=self.keyfile)
context.load_verify_locations(cafile=self.cacertificate)
# Certificates in ../kuksa_certificates does not contain the IP address used for
# connection to server so hostname check must be disabled
context.check_hostname = False
# We want host name to match
# For example certificates we use subjectAltName to make it match for Server, localahost and 127.0.0.1
# If using your own certificates make sure that name is included or use tls_server_name work-around
context.check_hostname = True
try:
print("connect to wss://"+self.serverIP+":"+str(self.serverPort))
async with websockets.connect("wss://"+self.serverIP+":"+str(self.serverPort), ssl=context) as ws:
args = {'uri':"wss://"+self.serverIP+":"+str(self.serverPort),'ssl':context}
# If your certificates does not contain the name of the server
# (for example during testing, in production it should match)
# then you can specify that checks should be against a different name
if self.tls_server_name:
args['server_hostname'] = self.tls_server_name

async with websockets.connect(**args) as ws:
print("Websocket connected securely.")
await self._msgHandler(ws)
except OSError as e:
Expand Down
Loading

0 comments on commit e96edd4

Please sign in to comment.