Merge branch 'feature/local_control_sec1' into 'master'

Added support for security1 in local control

See merge request espressif/esp-idf!13684
This commit is contained in:
Mahavir Jain 2021-08-27 08:31:33 +00:00
commit 3850eba152
9 changed files with 255 additions and 36 deletions

View File

@ -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
*/

View File

@ -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();

View File

@ -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
-------------------

View File

@ -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 ====

View File

@ -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')

View File

@ -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,

View File

@ -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!')

View File

@ -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)

View File

@ -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