diff --git a/components/esp_local_ctrl/include/esp_local_ctrl.h b/components/esp_local_ctrl/include/esp_local_ctrl.h index 9f4f89daa3..8322412eb3 100644 --- a/components/esp_local_ctrl/include/esp_local_ctrl.h +++ b/components/esp_local_ctrl/include/esp_local_ctrl.h @@ -228,6 +228,37 @@ typedef union { esp_local_ctrl_transport_config_httpd_t *httpd; } esp_local_ctrl_transport_config_t; +/** + * @brief Security types for esp_local_control + */ +typedef enum esp_local_ctrl_proto_sec { + PROTOCOM_SEC0 = 0, + PROTOCOM_SEC1, + PROTOCOM_SEC_CUSTOM, +} esp_local_ctrl_proto_sec_t; + +/** + * Protocom security configs + */ +typedef struct esp_local_ctrl_proto_sec_cfg { + /** + * This sets protocom security version, sec0/sec1 or custom + * If custom, user must provide handle via `proto_sec_custom_handle` below + */ + esp_local_ctrl_proto_sec_t version; + + /** + * Custom security handle if security is set custom via `proto_sec` above + * This handle must follow `protocomm_security_t` signature + */ + void *custom_handle; + + /** + * Proof of possession to be used for local control. Could be NULL. + */ + void *pop; +} esp_local_ctrl_proto_sec_cfg_t; + /** * @brief Configuration structure to pass to `esp_local_ctrl_start()` */ @@ -242,6 +273,11 @@ typedef struct esp_local_ctrl_config { */ esp_local_ctrl_transport_config_t transport_config; + /** + * Security version and POP + */ + esp_local_ctrl_proto_sec_cfg_t proto_sec; + /** * Register handlers for responding to get/set requests on properties */ diff --git a/components/esp_local_ctrl/src/esp_local_ctrl.c b/components/esp_local_ctrl/src/esp_local_ctrl.c index 1eead4d7c1..c99b96782b 100644 --- a/components/esp_local_ctrl/src/esp_local_ctrl.c +++ b/components/esp_local_ctrl/src/esp_local_ctrl.c @@ -19,6 +19,7 @@ #include #include +#include #include #include "esp_local_ctrl_priv.h" @@ -149,8 +150,21 @@ esp_err_t esp_local_ctrl_start(const esp_local_ctrl_config_t *config) return ret; } + protocomm_security_t *proto_sec_handle; + switch (local_ctrl_inst_ctx->config.proto_sec.version) { + case PROTOCOM_SEC_CUSTOM: + proto_sec_handle = local_ctrl_inst_ctx->config.proto_sec.custom_handle; + break; + case PROTOCOM_SEC1: + proto_sec_handle = (protocomm_security_t *) &protocomm_security1; + break; + case PROTOCOM_SEC0: + default: + proto_sec_handle = (protocomm_security_t *) &protocomm_security0; + break; + } ret = protocomm_set_security(local_ctrl_inst_ctx->pc, "esp_local_ctrl/session", - &protocomm_security0, NULL); + proto_sec_handle, local_ctrl_inst_ctx->config.proto_sec.pop); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to set session endpoint"); esp_local_ctrl_stop(); diff --git a/docs/en/api-reference/protocols/esp_local_ctrl.rst b/docs/en/api-reference/protocols/esp_local_ctrl.rst index 01df8a40a9..436c0753fe 100644 --- a/docs/en/api-reference/protocols/esp_local_ctrl.rst +++ b/docs/en/api-reference/protocols/esp_local_ctrl.rst @@ -24,6 +24,11 @@ Initialization of the **esp_local_ctrl** service over BLE transport is performed } } }, + .proto_sec = { + .version = PROTOCOM_SEC0, + .custom_handle = NULL, + .pop = NULL, + }, .handlers = { /* User defined handler functions */ .get_prop_values = get_property_values, @@ -65,6 +70,11 @@ Similarly for HTTPS transport: .transport_config = { .httpd = &https_conf }, + .proto_sec = { + .version = PROTOCOM_SEC0, + .custom_handle = NULL, + .pop = NULL, + }, .handlers = { /* User defined handler functions */ .get_prop_values = get_property_values, @@ -79,6 +89,11 @@ Similarly for HTTPS transport: /* Start esp_local_ctrl service */ ESP_ERROR_CHECK(esp_local_ctrl_start(&config)); +You may set security for transport in ESP local control using following options: + +1. `PROTOCOM_SEC1`: specifies that end to end encryption is used. +2. `PROTOCOM_SEC0`: specifies that data will be exchanged as a plain text. +3. `PROTOCOM_SEC_CUSTOM`: you can define your own security requirement. Please note that you will also have to provide `custom_handle` of type `protocomm_security_t *` in this context. Creating a property ------------------- diff --git a/examples/protocols/esp_local_ctrl/README.md b/examples/protocols/esp_local_ctrl/README.md index d9498c5b29..d0ca7d48a0 100644 --- a/examples/protocols/esp_local_ctrl/README.md +++ b/examples/protocols/esp_local_ctrl/README.md @@ -28,12 +28,12 @@ Sample output: After you've tested the name resolution, run: ``` -python scripts/esp_local_ctrl.py +python scripts/esp_local_ctrl.py --sec_ver 0 ``` Sample output: ``` -python scripts/esp_local_ctrl.py +python scripts/esp_local_ctrl.py --sec_ver 0 ==== Acquiring properties information ==== diff --git a/examples/protocols/esp_local_ctrl/example_test.py b/examples/protocols/esp_local_ctrl/example_test.py index ae4a644ed8..5aac681dd0 100644 --- a/examples/protocols/esp_local_ctrl/example_test.py +++ b/examples/protocols/esp_local_ctrl/example_test.py @@ -29,6 +29,7 @@ def test_examples_esp_local_ctrl(env, extra_data): # Running mDNS services in docker is not a trivial task. Therefore, the script won't connect to the host name but # to IP address. However, the certificates were generated for the host name and will be rejected. cmd = ' '.join([sys.executable, os.path.join(idf_path, rel_project_path, 'scripts/esp_local_ctrl.py'), + '--sec_ver 0', '--name', dut_ip, '--dont-check-hostname']) # don't reject the certificate because of the hostname esp_local_ctrl_log = os.path.join(idf_path, rel_project_path, 'esp_local_ctrl.log') diff --git a/examples/protocols/esp_local_ctrl/main/esp_local_ctrl_service.c b/examples/protocols/esp_local_ctrl/main/esp_local_ctrl_service.c index 2e80baeebb..db5a0748de 100644 --- a/examples/protocols/esp_local_ctrl/main/esp_local_ctrl_service.c +++ b/examples/protocols/esp_local_ctrl/main/esp_local_ctrl_service.c @@ -178,6 +178,11 @@ void start_esp_local_ctrl_service(void) .transport_config = { .httpd = &https_conf }, + .proto_sec = { + .version = 0, + .custom_handle = NULL, + .pop = NULL, + }, .handlers = { /* User defined handler functions */ .get_prop_values = get_property_values, diff --git a/examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py b/examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py index 95979b401f..44e786db46 100644 --- a/examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py +++ b/examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py @@ -18,19 +18,28 @@ from __future__ import print_function import argparse +import json import os import ssl import struct import sys +import textwrap from builtins import input -import proto +import proto_lc from future.utils import tobytes -# The tools directory is already in the PATH in environment prepared by install.sh which would allow to import -# esp_prov as file but not as complete module. -sys.path.insert(0, os.path.join(os.environ['IDF_PATH'], 'tools/esp_prov')) -import esp_prov # noqa: E402 +try: + import esp_prov + import security + +except ImportError: + idf_path = os.environ['IDF_PATH'] + sys.path.insert(0, idf_path + '/components/protocomm/python') + sys.path.insert(1, idf_path + '/tools/esp_prov') + + import esp_prov + import security # Set this to true to allow exceptions to be thrown config_throw_except = False @@ -118,6 +127,14 @@ def on_except(err): print(err) +def get_security(secver, pop=None, verbose=False): + if secver == 1: + return security.Security1(pop, verbose) + elif secver == 0: + return security.Security0(verbose) + return None + + def get_transport(sel_transport, service_name, check_hostname): try: tp = None @@ -140,29 +157,99 @@ def get_transport(sel_transport, service_name, check_hostname): return None -def version_match(tp, expected, verbose=False): +def version_match(tp, protover, verbose=False): try: - response = tp.send_data('esp_local_ctrl/version', expected) - return (response.lower() == expected.lower()) + response = tp.send_data('proto-ver', protover) + + if verbose: + print('proto-ver response : ', response) + + # First assume this to be a simple version string + if response.lower() == protover.lower(): + return True + + try: + # Else interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + if info['prov']['ver'].lower() == protover.lower(): + return True + + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False + except Exception as e: on_except(e) return None -def get_all_property_values(tp): +def has_capability(tp, capability='none', verbose=False): + # Note : default value of `capability` argument cannot be empty string + # because protocomm_httpd expects non zero content lengths + try: + response = tp.send_data('proto-ver', capability) + + if verbose: + print('proto-ver response : ', response) + + try: + # Interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + supported_capabilities = info['prov']['cap'] + if capability.lower() == 'none': + # No specific capability to check, but capabilities + # feature is present so return True + return True + elif capability in supported_capabilities: + return True + return False + + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False + + except RuntimeError as e: + on_except(e) + + return False + + +def establish_session(tp, sec): + try: + response = None + while True: + request = sec.security_session(response) + if request is None: + break + response = tp.send_data('esp_local_ctrl/session', request) + if (response is None): + return False + return True + except RuntimeError as e: + on_except(e) + return None + + +def get_all_property_values(tp, security_ctx): try: props = [] - message = proto.get_prop_count_request() + message = proto_lc.get_prop_count_request(security_ctx) response = tp.send_data('esp_local_ctrl/control', message) - count = proto.get_prop_count_response(response) + count = proto_lc.get_prop_count_response(security_ctx, response) if count == 0: raise RuntimeError('No properties found!') indices = [i for i in range(count)] - message = proto.get_prop_vals_request(indices) + message = proto_lc.get_prop_vals_request(security_ctx, indices) response = tp.send_data('esp_local_ctrl/control', message) - props = proto.get_prop_vals_response(response) + props = proto_lc.get_prop_vals_response(security_ctx, response) if len(props) != count: - raise RuntimeError('Incorrect count of properties!') + raise RuntimeError('Incorrect count of properties!', len(props), count) for p in props: p['value'] = decode_prop_value(p, p['value']) return props @@ -171,20 +258,27 @@ def get_all_property_values(tp): return [] -def set_property_values(tp, props, indices, values, check_readonly=False): +def set_property_values(tp, security_ctx, props, indices, values, check_readonly=False): try: if check_readonly: for index in indices: if prop_is_readonly(props[index]): raise RuntimeError('Cannot set value of Read-Only property') - message = proto.set_prop_vals_request(indices, values) + message = proto_lc.set_prop_vals_request(security_ctx, indices, values) response = tp.send_data('esp_local_ctrl/control', message) - return proto.set_prop_vals_response(response) + return proto_lc.set_prop_vals_response(security_ctx, response) except RuntimeError as e: on_except(e) return False +def desc_format(*args): + desc = '' + for arg in args: + desc += textwrap.fill(replace_whitespace=False, text=arg) + '\n' + return desc + + if __name__ == '__main__': parser = argparse.ArgumentParser(add_help=False) @@ -199,6 +293,22 @@ if __name__ == '__main__': parser.add_argument('--name', dest='service_name', type=str, help='BLE Device Name / HTTP Server hostname or IP', default='') + parser.add_argument('--sec_ver', dest='secver', type=int, default=None, + help=desc_format( + 'Protocomm security scheme used by the provisioning service for secure ' + 'session establishment. Accepted values are :', + '\t- 0 : No security', + '\t- 1 : X25519 key exchange + AES-CTR encryption', + '\t + Authentication using Proof of Possession (PoP)', + 'In case device side application uses IDF\'s provisioning manager, ' + 'the compatible security version is automatically determined from ' + 'capabilities retrieved via the version endpoint')) + + parser.add_argument('--pop', dest='pop', type=str, default='', + help=desc_format( + 'This specifies the Proof of possession (PoP) when security scheme 1 ' + 'is used')) + parser.add_argument('--dont-check-hostname', action='store_true', # If enabled, the certificate won't be rejected for hostname mismatch. # This option is hidden because it should be used only for testing purposes. @@ -220,6 +330,31 @@ if __name__ == '__main__': print('---- Invalid transport ----') exit(1) + # If security version not specified check in capabilities + if args.secver is None: + # First check if capabilities are supported or not + if not has_capability(obj_transport): + print('Security capabilities could not be determined. Please specify \'--sec_ver\' explicitly') + print('---- Invalid Security Version ----') + exit(2) + + # When no_sec is present, use security 0, else security 1 + args.secver = int(not has_capability(obj_transport, 'no_sec')) + print('Security scheme determined to be :', args.secver) + + if (args.secver != 0) and not has_capability(obj_transport, 'no_pop'): + if len(args.pop) == 0: + print('---- Proof of Possession argument not provided ----') + exit(2) + elif len(args.pop) != 0: + print('---- Proof of Possession will be ignored ----') + args.pop = '' + + obj_security = get_security(args.secver, args.pop, False) + if obj_security is None: + print('---- Invalid Security Version ----') + exit(2) + if args.version != '': print('\n==== Verifying protocol version ====') if not version_match(obj_transport, args.version, args.verbose): @@ -227,8 +362,15 @@ if __name__ == '__main__': exit(2) print('==== Verified protocol version successfully ====') + print('\n==== Starting Session ====') + if not establish_session(obj_transport, obj_security): + print('Failed to establish session. Ensure that security scheme and proof of possession are correct') + print('---- Error in establishing session ----') + exit(3) + print('==== Session Established ====') + while True: - properties = get_all_property_values(obj_transport) + properties = get_all_property_values(obj_transport, obj_security) if len(properties) == 0: print('---- Error in reading property values ----') exit(4) @@ -245,7 +387,7 @@ if __name__ == '__main__': select = 0 while True: try: - inval = input("\nSelect properties to set (0 to re-read, 'q' to quit) : ") + inval = input('\nSelect properties to set (0 to re-read, \'q\' to quit) : ') if inval.lower() == 'q': print('Quitting...') exit(5) @@ -274,5 +416,5 @@ if __name__ == '__main__': set_values += [value] set_indices += [select - 1] - if not set_property_values(obj_transport, properties, set_indices, set_values): + if not set_property_values(obj_transport, obj_security, properties, set_indices, set_values): print('Failed to set values!') diff --git a/examples/protocols/esp_local_ctrl/scripts/proto.py b/examples/protocols/esp_local_ctrl/scripts/proto_lc.py similarity index 75% rename from examples/protocols/esp_local_ctrl/scripts/proto.py rename to examples/protocols/esp_local_ctrl/scripts/proto_lc.py index dcad41d1b9..71ed3a987e 100644 --- a/examples/protocols/esp_local_ctrl/scripts/proto.py +++ b/examples/protocols/esp_local_ctrl/scripts/proto_lc.py @@ -36,35 +36,39 @@ constants_pb2 = _load_source('constants_pb2', idf_path + '/components/protocomm/ local_ctrl_pb2 = _load_source('esp_local_ctrl_pb2', idf_path + '/components/esp_local_ctrl/python/esp_local_ctrl_pb2.py') -def get_prop_count_request(): +def get_prop_count_request(security_ctx): req = local_ctrl_pb2.LocalCtrlMessage() req.msg = local_ctrl_pb2.TypeCmdGetPropertyCount payload = local_ctrl_pb2.CmdGetPropertyCount() req.cmd_get_prop_count.MergeFrom(payload) - return req.SerializeToString() + enc_cmd = security_ctx.encrypt_data(req.SerializeToString()) + return enc_cmd -def get_prop_count_response(response_data): +def get_prop_count_response(security_ctx, response_data): + decrypt = security_ctx.decrypt_data(tobytes(response_data)) resp = local_ctrl_pb2.LocalCtrlMessage() - resp.ParseFromString(tobytes(response_data)) + resp.ParseFromString(decrypt) if (resp.resp_get_prop_count.status == 0): return resp.resp_get_prop_count.count else: return 0 -def get_prop_vals_request(indices): +def get_prop_vals_request(security_ctx, indices): req = local_ctrl_pb2.LocalCtrlMessage() req.msg = local_ctrl_pb2.TypeCmdGetPropertyValues payload = local_ctrl_pb2.CmdGetPropertyValues() payload.indices.extend(indices) req.cmd_get_prop_vals.MergeFrom(payload) - return req.SerializeToString() + enc_cmd = security_ctx.encrypt_data(req.SerializeToString()) + return enc_cmd -def get_prop_vals_response(response_data): +def get_prop_vals_response(security_ctx, response_data): + decrypt = security_ctx.decrypt_data(tobytes(response_data)) resp = local_ctrl_pb2.LocalCtrlMessage() - resp.ParseFromString(tobytes(response_data)) + resp.ParseFromString(decrypt) results = [] if (resp.resp_get_prop_vals.status == 0): for prop in resp.resp_get_prop_vals.props: @@ -77,7 +81,7 @@ def get_prop_vals_response(response_data): return results -def set_prop_vals_request(indices, values): +def set_prop_vals_request(security_ctx, indices, values): req = local_ctrl_pb2.LocalCtrlMessage() req.msg = local_ctrl_pb2.TypeCmdSetPropertyValues payload = local_ctrl_pb2.CmdSetPropertyValues() @@ -86,10 +90,12 @@ def set_prop_vals_request(indices, values): prop.index = i prop.value = v req.cmd_set_prop_vals.MergeFrom(payload) - return req.SerializeToString() + enc_cmd = security_ctx.encrypt_data(req.SerializeToString()) + return enc_cmd -def set_prop_vals_response(response_data): +def set_prop_vals_response(security_ctx, response_data): + decrypt = security_ctx.decrypt_data(tobytes(response_data)) resp = local_ctrl_pb2.LocalCtrlMessage() - resp.ParseFromString(tobytes(response_data)) + resp.ParseFromString(decrypt) return (resp.resp_set_prop_vals.status == 0) diff --git a/tools/ci/mypy_ignore_list.txt b/tools/ci/mypy_ignore_list.txt index 8227e8eebd..69c6834f0e 100644 --- a/tools/ci/mypy_ignore_list.txt +++ b/tools/ci/mypy_ignore_list.txt @@ -73,7 +73,7 @@ examples/protocols/cbor/example_test.py examples/protocols/esp_http_client/esp_http_client_test.py examples/protocols/esp_local_ctrl/example_test.py examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py -examples/protocols/esp_local_ctrl/scripts/proto.py +examples/protocols/esp_local_ctrl/scripts/proto_lc.py examples/protocols/http_server/advanced_tests/http_server_advanced_test.py examples/protocols/http_server/advanced_tests/scripts/test.py examples/protocols/http_server/persistent_sockets/http_server_persistence_test.py