Added support for security1 in local control

1. Added config options to chose from protocom security.
    It can be chosen 0/1 or custom.
    Possible to set POP as well

2. Added support in `esp_local_ctrl.py` test script for sec_ver selection

Signed-off-by: Vikram Dattu <vikram.dattu@espressif.com>
This commit is contained in:
Vikram Dattu
2021-05-31 11:05:21 +05:30
committed by bot
parent 2fa5cee80d
commit 3af5f20da1
7 changed files with 244 additions and 38 deletions

View File

@ -228,6 +228,37 @@ typedef union {
esp_local_ctrl_transport_config_httpd_t *httpd; esp_local_ctrl_transport_config_httpd_t *httpd;
} esp_local_ctrl_transport_config_t; } 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()` * @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; 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 * Register handlers for responding to get/set requests on properties
*/ */

View File

@ -19,6 +19,7 @@
#include <protocomm.h> #include <protocomm.h>
#include <protocomm_security0.h> #include <protocomm_security0.h>
#include <protocomm_security1.h>
#include <esp_local_ctrl.h> #include <esp_local_ctrl.h>
#include "esp_local_ctrl_priv.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; 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", 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) { if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to set session endpoint"); ESP_LOGE(TAG, "Failed to set session endpoint");
esp_local_ctrl_stop(); esp_local_ctrl_stop();

View File

@ -28,12 +28,12 @@ Sample output:
After you've tested the name resolution, run: 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: Sample output:
``` ```
python scripts/esp_local_ctrl.py python scripts/esp_local_ctrl.py --sec_ver 0
==== Acquiring properties information ==== ==== Acquiring properties information ====

View File

@ -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 # 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. # 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'), cmd = ' '.join([sys.executable, os.path.join(idf_path, rel_project_path, 'scripts/esp_local_ctrl.py'),
'--sec_ver 0',
'--name', dut_ip, '--name', dut_ip,
'--dont-check-hostname']) # don't reject the certificate because of the hostname '--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') esp_local_ctrl_log = os.path.join(idf_path, rel_project_path, 'esp_local_ctrl.log')

View File

@ -178,6 +178,11 @@ void start_esp_local_ctrl_service(void)
.transport_config = { .transport_config = {
.httpd = &https_conf .httpd = &https_conf
}, },
.proto_sec = {
.version = 0,
.custom_handle = NULL,
.pop = NULL,
},
.handlers = { .handlers = {
/* User defined handler functions */ /* User defined handler functions */
.get_prop_values = get_property_values, .get_prop_values = get_property_values,

View File

@ -18,18 +18,29 @@
from __future__ import print_function from __future__ import print_function
from future.utils import tobytes from future.utils import tobytes
from builtins import input from builtins import input
import os import os
import sys import sys
import struct import struct
import argparse import argparse
import json
import ssl import ssl
import proto import textwrap
# The tools directory is already in the PATH in environment prepared by install.sh which would allow to import import proto_lc
# esp_prov as file but not as complete module.
sys.path.insert(0, os.path.join(os.environ['IDF_PATH'], 'tools/esp_prov')) try:
import esp_prov # noqa: E402 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 # Set this to true to allow exceptions to be thrown
@ -118,6 +129,14 @@ def on_except(err):
print(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): def get_transport(sel_transport, service_name, check_hostname):
try: try:
tp = None tp = None
@ -140,29 +159,99 @@ def get_transport(sel_transport, service_name, check_hostname):
return None return None
def version_match(tp, expected, verbose=False): def version_match(tp, protover, verbose=False):
try: try:
response = tp.send_data('esp_local_ctrl/version', expected) response = tp.send_data('proto-ver', protover)
return (response.lower() == expected.lower())
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: except Exception as e:
on_except(e) on_except(e)
return None 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: try:
props = [] 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) 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: if count == 0:
raise RuntimeError("No properties found!") raise RuntimeError("No properties found!")
indices = [i for i in range(count)] 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) 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: if len(props) != count:
raise RuntimeError("Incorrect count of properties!") raise RuntimeError('Incorrect count of properties!', len(props), count)
for p in props: for p in props:
p["value"] = decode_prop_value(p, p["value"]) p["value"] = decode_prop_value(p, p["value"])
return props return props
@ -171,20 +260,27 @@ def get_all_property_values(tp):
return [] 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: try:
if check_readonly: if check_readonly:
for index in indices: for index in indices:
if prop_is_readonly(props[index]): if prop_is_readonly(props[index]):
raise RuntimeError("Cannot set value of Read-Only property") 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) 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: except RuntimeError as e:
on_except(e) on_except(e)
return False 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__': if __name__ == '__main__':
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
@ -199,7 +295,23 @@ if __name__ == '__main__':
parser.add_argument("--name", dest='service_name', type=str, parser.add_argument("--name", dest='service_name', type=str,
help="BLE Device Name / HTTP Server hostname or IP", default='') 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. # If enabled, the certificate won't be rejected for hostname mismatch.
# This option is hidden because it should be used only for testing purposes. # This option is hidden because it should be used only for testing purposes.
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
@ -220,6 +332,31 @@ if __name__ == '__main__':
print("---- Invalid transport ----") print("---- Invalid transport ----")
exit(1) 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 != '': if args.version != '':
print("\n==== Verifying protocol version ====") print("\n==== Verifying protocol version ====")
if not version_match(obj_transport, args.version, args.verbose): if not version_match(obj_transport, args.version, args.verbose):
@ -227,8 +364,15 @@ if __name__ == '__main__':
exit(2) exit(2)
print("==== Verified protocol version successfully ====") 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: while True:
properties = get_all_property_values(obj_transport) properties = get_all_property_values(obj_transport, obj_security)
if len(properties) == 0: if len(properties) == 0:
print("---- Error in reading property values ----") print("---- Error in reading property values ----")
exit(4) exit(4)
@ -245,7 +389,7 @@ if __name__ == '__main__':
select = 0 select = 0
while True: while True:
try: 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': if inval.lower() == 'q':
print("Quitting...") print("Quitting...")
exit(5) exit(5)
@ -274,5 +418,5 @@ if __name__ == '__main__':
set_values += [value] set_values += [value]
set_indices += [select - 1] 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!") print('Failed to set values!')

View File

@ -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") 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 = local_ctrl_pb2.LocalCtrlMessage()
req.msg = local_ctrl_pb2.TypeCmdGetPropertyCount req.msg = local_ctrl_pb2.TypeCmdGetPropertyCount
payload = local_ctrl_pb2.CmdGetPropertyCount() payload = local_ctrl_pb2.CmdGetPropertyCount()
req.cmd_get_prop_count.MergeFrom(payload) 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 = local_ctrl_pb2.LocalCtrlMessage()
resp.ParseFromString(tobytes(response_data)) resp.ParseFromString(decrypt)
if (resp.resp_get_prop_count.status == 0): if (resp.resp_get_prop_count.status == 0):
return resp.resp_get_prop_count.count return resp.resp_get_prop_count.count
else: else:
return 0 return 0
def get_prop_vals_request(indices): def get_prop_vals_request(security_ctx, indices):
req = local_ctrl_pb2.LocalCtrlMessage() req = local_ctrl_pb2.LocalCtrlMessage()
req.msg = local_ctrl_pb2.TypeCmdGetPropertyValues req.msg = local_ctrl_pb2.TypeCmdGetPropertyValues
payload = local_ctrl_pb2.CmdGetPropertyValues() payload = local_ctrl_pb2.CmdGetPropertyValues()
payload.indices.extend(indices) payload.indices.extend(indices)
req.cmd_get_prop_vals.MergeFrom(payload) 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 = local_ctrl_pb2.LocalCtrlMessage()
resp.ParseFromString(tobytes(response_data)) resp.ParseFromString(decrypt)
results = [] results = []
if (resp.resp_get_prop_vals.status == 0): if (resp.resp_get_prop_vals.status == 0):
for prop in resp.resp_get_prop_vals.props: for prop in resp.resp_get_prop_vals.props:
@ -75,7 +79,7 @@ def get_prop_vals_response(response_data):
return results return results
def set_prop_vals_request(indices, values): def set_prop_vals_request(security_ctx, indices, values):
req = local_ctrl_pb2.LocalCtrlMessage() req = local_ctrl_pb2.LocalCtrlMessage()
req.msg = local_ctrl_pb2.TypeCmdSetPropertyValues req.msg = local_ctrl_pb2.TypeCmdSetPropertyValues
payload = local_ctrl_pb2.CmdSetPropertyValues() payload = local_ctrl_pb2.CmdSetPropertyValues()
@ -84,10 +88,12 @@ def set_prop_vals_request(indices, values):
prop.index = i prop.index = i
prop.value = v prop.value = v
req.cmd_set_prop_vals.MergeFrom(payload) 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 = local_ctrl_pb2.LocalCtrlMessage()
resp.ParseFromString(tobytes(response_data)) resp.ParseFromString(decrypt)
return (resp.resp_set_prop_vals.status == 0) return (resp.resp_set_prop_vals.status == 0)