Explorar el Código

Merge branch 'feature/local_control_sec1_v4.2' into 'release/v4.2'

Added support for security1 in local control (backport v4.2)

See merge request espressif/esp-idf!15280
Vikram hace 4 años
padre
commit
c327a0016e

+ 36 - 0
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
      */

+ 15 - 1
components/esp_local_ctrl/src/esp_local_ctrl.c

@@ -19,6 +19,7 @@
 
 #include <protocomm.h>
 #include <protocomm_security0.h>
+#include <protocomm_security1.h>
 
 #include <esp_local_ctrl.h>
 #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();

+ 15 - 0
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
 -------------------

+ 2 - 2
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 ====
 

+ 1 - 0
examples/protocols/esp_local_ctrl/example_test.py

@@ -27,6 +27,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')

+ 5 - 0
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,

+ 167 - 23
examples/protocols/esp_local_ctrl/scripts/esp_local_ctrl.py

@@ -18,18 +18,29 @@
 from __future__ import print_function
 from future.utils import tobytes
 from builtins import input
+
 import os
 import sys
 import struct
 import argparse
+import json
 import ssl
 
-import proto
+import textwrap
+
+import proto_lc
+
+try:
+    import esp_prov
+    import security
 
-# 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
+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
@@ -118,6 +129,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 +159,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 +260,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)
+                    raise RuntimeError('Cannot set value of Read-Only property')
+        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,7 +295,23 @@ 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("--dont-check-hostname", action="store_true",
+    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.
                         help=argparse.SUPPRESS)
@@ -220,6 +332,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 +364,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 +389,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 +418,5 @@ if __name__ == '__main__':
             set_values += [value]
             set_indices += [select - 1]
 
-        if not set_property_values(obj_transport, properties, set_indices, set_values):
-            print("Failed to set values!")
+        if not set_property_values(obj_transport, obj_security, properties, set_indices, set_values):
+            print('Failed to set values!')

+ 18 - 12
examples/protocols/esp_local_ctrl/scripts/proto.py → examples/protocols/esp_local_ctrl/scripts/proto_lc.py

@@ -34,35 +34,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:
@@ -75,7 +79,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()
@@ -84,10 +88,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)