diff --git a/examples/light_switch/README.md b/examples/light_switch/README.md index b695e64c5..59e821964 100644 --- a/examples/light_switch/README.md +++ b/examples/light_switch/README.md @@ -201,6 +201,19 @@ available for you to run your application's code. Applications that do not require BLE post commissioning, can disable it using app_ble_disable() once commissioning is complete. It is not done explicitly because of a known issue with esp32c3 and will be fixed with the next IDF release (v4.4.2). +## 4. Dynamic Passcode + +If the device features a screen capable of displaying the pairing QR Code, it is advisable to utilize a dynamic passcode for this purpose as the static passcode shall conform to more stringent rules. To enable the use of a dynamic passcode in the example, please ensure that the following configuration options are activated. + +``` +CONFIG_CUSTOM_COMMISSIONABLE_DATA_PROVIDER=y +CONFIG_DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER=y +``` +After implementing these configurations, the device will generate a new, random passcode every time it reboots, if it is not yet commissioned. To obtain the commissioning QR Code, enter `matter onboardingcodes ble qrcode` in the device console, and then initiate the pairing process. +``` +./chip-tool pairing code-wifi 1 +``` + ## A2 Appendix FAQs ### A2.1 Binding Failed diff --git a/examples/light_switch/main/CMakeLists.txt b/examples/light_switch/main/CMakeLists.txt index f48a36750..76c4fe44b 100644 --- a/examples/light_switch/main/CMakeLists.txt +++ b/examples/light_switch/main/CMakeLists.txt @@ -1,4 +1,10 @@ -idf_component_register(SRC_DIRS "." +set(SRC_DIRS_LIST ".") + +if (CONFIG_DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER) + list(APPEND SRC_DIRS_LIST "custom_provider") +endif() + +idf_component_register(SRC_DIRS ${SRC_DIRS_LIST} PRIV_INCLUDE_DIRS ".") set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/examples/light_switch/main/Kconfig.projbuild b/examples/light_switch/main/Kconfig.projbuild new file mode 100644 index 000000000..0db7e6d9f --- /dev/null +++ b/examples/light_switch/main/Kconfig.projbuild @@ -0,0 +1,32 @@ +menu "Example Configuration" + visible if CUSTOM_COMMISSIONABLE_DATA_PROVIDER + + config DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER + bool "Enable Dynamic Passcode Commissionable Data Provider" + depends on CUSTOM_COMMISSIONABLE_DATA_PROVIDER + default y + + config DYNAMIC_PASSCODE_PROVIDER_DISCRIMINATOR + int "Discriminator in Dynamic Passcode Commissionable Data Provider" + depends on DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER + default 3840 + range 0 4095 + help + Fixed discriminator in custom dynamic passcode commissionable data provider + + config DYNAMIC_PASSCODE_PROVIDER_ITERATIONS + int "Iterations in Dynamic Passcode Commissionable Data Provider" + depends on DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER + default 10000 + range 1000 100000 + help + Fixed iterations in custom dynamic passcode commissionable data provider + + config DYNAMIC_PASSCODE_PROVIDER_SALT_BASE64 + string "Base64-Encoded Salt in Dynamic Passcode Commissionable Data Provider" + depends on DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER + default "0NHS09TV1tfY2drb3N3e36ChoqOkpaanqKmqq6ytrq8=" + help + Fixed salt in custom dynamic passcode commissionable data provider. It should be a Base64-Encoded string. + +endmenu diff --git a/examples/light_switch/main/app_main.cpp b/examples/light_switch/main/app_main.cpp index f84edc957..cb97da585 100644 --- a/examples/light_switch/main/app_main.cpp +++ b/examples/light_switch/main/app_main.cpp @@ -13,12 +13,16 @@ #include #include #include +#include #include #include #if CHIP_DEVICE_CONFIG_ENABLE_THREAD #include #endif +#if CONFIG_DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER +#include +#endif static const char *TAG = "app_main"; uint16_t switch_endpoint_id = 0; @@ -27,6 +31,10 @@ using namespace esp_matter; using namespace esp_matter::attribute; using namespace esp_matter::endpoint; +#if CONFIG_DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER +dynamic_commissionable_data_provider g_dynamic_passcode_provider; +#endif + static void app_event_cb(const ChipDeviceEvent *event, intptr_t arg) { switch (event->Type) { @@ -125,6 +133,11 @@ extern "C" void app_main() set_openthread_platform_config(&config); #endif +#if CONFIG_DYNAMIC_PASSCODE_COMMISSIONABLE_DATA_PROVIDER + /* This should be called before esp_matter::start() */ + esp_matter::set_custom_commissionable_data_provider(&g_dynamic_passcode_provider); +#endif + /* Matter start */ err = esp_matter::start(app_event_cb); if (err != ESP_OK) { diff --git a/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.cpp b/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.cpp new file mode 100644 index 000000000..220803e16 --- /dev/null +++ b/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.cpp @@ -0,0 +1,115 @@ +// Copyright 2023 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include + +using namespace ::chip::DeviceLayer::Internal; +using namespace ::chip; + +constexpr char *TAG = "custom_provider"; + +CHIP_ERROR dynamic_commissionable_data_provider::GetSetupDiscriminator(uint16_t &setupDiscriminator) +{ + setupDiscriminator = CONFIG_DYNAMIC_PASSCODE_PROVIDER_DISCRIMINATOR; + return CHIP_NO_ERROR; +} + +CHIP_ERROR dynamic_commissionable_data_provider::GetSpake2pIterationCount(uint32_t &iterationCount) +{ + iterationCount = CONFIG_DYNAMIC_PASSCODE_PROVIDER_ITERATIONS; + return CHIP_NO_ERROR; +} + +static bool is_valid_base64_str(const char *str) +{ + const char *base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (!str) { + return false; + } + size_t len = strlen(str); + if (len % 4 != 0) { + return false; + } + size_t padding_len = 0; + if (str[len - 1] == '=') { + padding_len++; + if (str[len - 2] == '=') { + padding_len++; + } + } + for (size_t i = 0; i < len - padding_len; ++i) { + if (strchr(base64_chars, str[i]) == NULL) { + return false; + } + } + return true; +} + +CHIP_ERROR dynamic_commissionable_data_provider::GetSpake2pSalt(MutableByteSpan &saltBuf) +{ + const char *saltB64 = CONFIG_DYNAMIC_PASSCODE_PROVIDER_SALT_BASE64; + ReturnErrorCodeIf(!is_valid_base64_str(saltB64), CHIP_ERROR_INVALID_ARGUMENT); + size_t saltB64Len = strlen(saltB64); + uint8_t salt[chip::Crypto::kSpake2p_Max_PBKDF_Salt_Length]; + size_t saltLen = chip::Base64Decode32(saltB64, saltB64Len, salt); + ReturnErrorCodeIf(saltLen < chip::Crypto::kSpake2p_Min_PBKDF_Salt_Length, CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorCodeIf(saltLen > saltBuf.size(), CHIP_ERROR_BUFFER_TOO_SMALL); + + memcpy(saltBuf.data(), salt, saltLen); + saltBuf.reduce_size(saltLen); + return CHIP_NO_ERROR; +} + +CHIP_ERROR dynamic_commissionable_data_provider::GetSpake2pVerifier(MutableByteSpan &verifierBuf, size_t &verifierLen) +{ + uint32_t setupPasscode = 0; + uint32_t iterationCount = 0; + uint8_t salt[Crypto::kSpake2p_Max_PBKDF_Salt_Length] = {0}; + chip::MutableByteSpan saltSpan(salt, Crypto::kSpake2p_Max_PBKDF_Salt_Length); + ReturnErrorOnFailure(GetSetupPasscode(setupPasscode)); + ReturnErrorOnFailure(GetSpake2pIterationCount(iterationCount)); + ReturnErrorOnFailure(GetSpake2pSalt(saltSpan)); + chip::Crypto::Spake2pVerifier verifier; + ReturnErrorOnFailure(verifier.Generate(iterationCount, saltSpan, setupPasscode)); + ReturnErrorOnFailure(verifier.Serialize(verifierBuf)); + verifierLen = verifierBuf.size(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR dynamic_commissionable_data_provider::GetSetupPasscode(uint32_t &setupPasscode) +{ + if (mSetupPasscode == 0) { + ReturnErrorOnFailure(GenerateRandomPasscode(mSetupPasscode)); + } + setupPasscode = mSetupPasscode; + return CHIP_NO_ERROR; +} + +CHIP_ERROR dynamic_commissionable_data_provider::GenerateRandomPasscode(uint32_t &passcode) +{ + ReturnErrorOnFailure(chip::Crypto::DRBG_get_bytes(reinterpret_cast(&passcode), sizeof(passcode))); + // Passcode MUST be 1 to 99999998 + passcode = (passcode % chip::kSetupPINCodeMaximumValue) + 1; + if (!chip::SetupPayload::IsValidSetupPIN(passcode)) { + // if the generated passcode is invalid (11111111, 22222222, 33333333, 44444444, 55555555, 66666666, + // 77777777, 88888888, 12345678, 87654321), increase it by 1 to make it valid. + passcode = passcode + 1; + } + return CHIP_NO_ERROR; +} diff --git a/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.h b/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.h new file mode 100644 index 000000000..602ba24ad --- /dev/null +++ b/examples/light_switch/main/custom_provider/dynamic_commissionable_data_provider.h @@ -0,0 +1,39 @@ +// Copyright 2023 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +using chip::MutableByteSpan; +using chip::DeviceLayer::CommissionableDataProvider; + +class dynamic_commissionable_data_provider : public CommissionableDataProvider { +public: + dynamic_commissionable_data_provider() + : CommissionableDataProvider() {} + + // Members functions that implement the CommissionableDataProvider + CHIP_ERROR GetSetupDiscriminator(uint16_t &setupDiscriminator) override; + CHIP_ERROR SetSetupDiscriminator(uint16_t setupDiscriminator) override { return CHIP_ERROR_NOT_IMPLEMENTED; } + CHIP_ERROR GetSpake2pIterationCount(uint32_t &iterationCount) override; + CHIP_ERROR GetSpake2pSalt(MutableByteSpan &saltBuf) override; + CHIP_ERROR GetSpake2pVerifier(MutableByteSpan &verifierBuf, size_t &verifierLen) override; + CHIP_ERROR GetSetupPasscode(uint32_t &setupPasscode) override; + CHIP_ERROR SetSetupPasscode(uint32_t setupPasscode) override { return CHIP_ERROR_NOT_IMPLEMENTED; } +private: + CHIP_ERROR GenerateRandomPasscode(uint32_t &passcode); + uint32_t mSetupPasscode = 0; +}; diff --git a/tools/mfg_tool/chip_nvs.py b/tools/mfg_tool/chip_nvs.py index ffec2c389..4f29a2975 100644 --- a/tools/mfg_tool/chip_nvs.py +++ b/tools/mfg_tool/chip_nvs.py @@ -41,11 +41,6 @@ CHIP_NVS_MAP = { 'encoding': 'string', 'value': None, }, - 'verifier': { - 'type': 'data', - 'encoding': 'string', - 'value': None, - }, } } diff --git a/tools/mfg_tool/mfg_tool.py b/tools/mfg_tool/mfg_tool.py index b560e3e42..19c40712c 100755 --- a/tools/mfg_tool/mfg_tool.py +++ b/tools/mfg_tool/mfg_tool.py @@ -99,17 +99,23 @@ def generate_passcodes(args): salt_len_max = 32 with open(OUT_FILE['pin_csv'], 'w', newline='') as f: writer = csv.writer(f) - writer.writerow(["Index", "PIN Code", "Iteration Count", "Salt", "Verifier"]) + if args.enable_dynamic_passcode: + writer.writerow(["Index", "Iteration Count", "Salt"]) + else: + writer.writerow(["Index", "PIN Code", "Iteration Count", "Salt", "Verifier"]) for i in range(0, args.count): - if args.passcode: - passcode = args.passcode - else: - passcode = random.randint(1, 99999998) - if passcode in INVALID_PASSCODES: - passcode -= 1 salt = os.urandom(salt_len_max) - verifier = generate_verifier(passcode, salt, iter_count_max) - writer.writerow([i, passcode, iter_count_max, base64.b64encode(salt).decode('utf-8'), base64.b64encode(verifier).decode('utf-8')]) + if args.enable_dynamic_passcode: + writer.writerow([i, iter_count_max, base64.b64encode(salt).decode('utf-8')]) + else: + if args.passcode: + passcode = args.passcode + else: + passcode = random.randint(1, 99999998) + if passcode in INVALID_PASSCODES: + passcode -= 1 + verifier = generate_verifier(passcode, salt, iter_count_max) + writer.writerow([i, passcode, iter_count_max, base64.b64encode(salt).decode('utf-8'), base64.b64encode(verifier).decode('utf-8')]) def generate_discriminators(args): @@ -187,7 +193,7 @@ def generate_pai(args, ca_key, ca_cert, out_key, out_cert): logging.info('Generated PAI private key: {}'.format(out_key)) -def generate_dac(iteration, args, discriminator, passcode, ca_key, ca_cert): +def generate_dac(iteration, args, ca_key, ca_cert): out_key_pem = os.sep.join([OUT_DIR['top'], UUIDs[iteration], 'internal', 'DAC_key.pem']) out_private_key_der = out_key_pem.replace('key.pem', 'key.der') out_cert_pem = out_key_pem.replace('key.pem', 'cert.pem') @@ -338,13 +344,13 @@ def write_per_device_unique_data(args): chip_factory_update('discriminator', row['Discriminator']) chip_factory_update('iteration-count', row['Iteration Count']) chip_factory_update('salt', row['Salt']) - chip_factory_update('verifier', row['Verifier']) + if not args.enable_dynamic_passcode: + chip_factory_update('verifier', row['Verifier']) if args.paa or args.pai: if args.dac_key is not None and args.dac_cert is not None: dacs = use_dac_from_args(args) else: - dacs = generate_dac(int(row['Index']), args, int(row['Discriminator']), - int(row['PIN Code']), PAI['key_pem'], PAI['cert_pem']) + dacs = generate_dac(int(row['Index']), args, PAI['key_pem'], PAI['cert_pem']) if not args.dac_in_secure_cert: chip_factory_update('dac-cert', os.path.abspath(dacs[0])) @@ -394,7 +400,8 @@ def write_per_device_unique_data(args): append_chip_mcsv_row(mcsv_row_data) # Generate onboarding data - generate_onboarding_data(args, int(row['Index']), int(chip_factory_get_val('discriminator')), int(row['PIN Code'])) + if not args.enable_dynamic_passcode: + generate_onboarding_data(args, int(row['Index']), int(chip_factory_get_val('discriminator')), int(row['PIN Code'])) if args.paa or args.pai: logging.info("Generated CSV of Common Name and DAC: {}".format(OUT_FILE['cn_dac_csv'])) @@ -435,22 +442,29 @@ def generate_summary(args): summary_csv_data = '' with open(master_csv, 'r') as mcsvf: summary_lines = mcsvf.read().splitlines() - summary_csv_data += summary_lines[0] + ',pincode,qrcode,manualcode\n' + summary_csv_data += summary_lines[0] + if not args.enable_dynamic_passcode: + summary_csv_data += ',pincode,qrcode,manualcode\n' + else: + summary_csv_data += '\n' with open(OUT_FILE['pin_disc_csv'], 'r') as pdcsvf: pin_disc_dict = csv.DictReader(pdcsvf) for row in pin_disc_dict: - pincode = row['PIN Code'] - discriminator = row['Discriminator'] - payloads = SetupPayload(int(discriminator), int(pincode), 1 << args.discovery_mode, CommissioningFlow(args.commissioning_flow), - args.vendor_id, args.product_id) - qrcode = payloads.generate_qrcode() - manualcode = payloads.generate_manualcode() - # ToDo: remove this if qrcode tool can handle the standard manual code format - if args.commissioning_flow == CommissioningFlow.Standard: - manualcode = manualcode[:4] + '-' + manualcode[4:7] + '-' + manualcode[7:] + if not args.enable_dynamic_passcode: + pincode = row['PIN Code'] + discriminator = row['Discriminator'] + payloads = SetupPayload(int(discriminator), int(pincode), 1 << args.discovery_mode, CommissioningFlow(args.commissioning_flow), + args.vendor_id, args.product_id) + qrcode = payloads.generate_qrcode() + manualcode = payloads.generate_manualcode() + # ToDo: remove this if qrcode tool can handle the standard manual code format + if args.commissioning_flow == CommissioningFlow.Standard: + manualcode = manualcode[:4] + '-' + manualcode[4:7] + '-' + manualcode[7:] + else: + manualcode = '"' + manualcode[:4] + '-' + manualcode[4:7] + '-' + manualcode[7:11] + '\n' + manualcode[11:15] + '-' + manualcode[15:18] + '-' + manualcode[18:20] + '-' + manualcode[20:21] + '"' + summary_csv_data += summary_lines[1 + int(row['Index'])] + ',' + pincode + ',' + qrcode + ',' + manualcode + '\n' else: - manualcode = '"' + manualcode[:4] + '-' + manualcode[4:7] + '-' + manualcode[7:11] + '\n' + manualcode[11:15] + '-' + manualcode[15:18] + '-' + manualcode[18:20] + '-' + manualcode[20:21] + '"' - summary_csv_data += summary_lines[1 + int(row['Index'])] + ',' + pincode + ',' + qrcode + ',' + manualcode + '\n' + summary_csv_data += summary_lines[1 + int(row['Index'])] + '\n' with open(summary_csv, 'w') as scsvf: scsvf.write(summary_csv_data) @@ -528,6 +542,11 @@ def get_args(): g_commissioning.add_argument('-dm', '--discovery-mode', type=any_base_int, default=1, help='Commissionable device discovery networking technology. \ 0:WiFi-SoftAP, 1:BLE, 2:On-network. Default is BLE.', choices=[0, 1, 2]) + g_commissioning.add_argument('--enable-dynamic-passcode', action="store_true", required=False, + help='Enable dynamic passcode. If enabling this option, the generated binaries will \ + not include the spake2p verifier. so this option should work with a custom \ + CommissionableDataProvider which can generate random passcode and \ + corresponding verifier') g_dac = parser.add_argument_group('Device attestation credential options') g_dac.add_argument('--dac-in-secure-cert', action="store_true", required=False, @@ -623,6 +642,9 @@ def add_optional_KVs(args): chip_factory_append('dac-pub-key', 'file', 'binary', None) chip_factory_append('pai-cert', 'file', 'binary', None) + if not args.enable_dynamic_passcode: + chip_factory_append('verifier', 'data', 'string', None) + # Add certificate declaration if args.cert_dclrn: chip_factory_append('cert-dclrn','file','binary', os.path.relpath(args.cert_dclrn))