mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
provisioning: Added documentation for SRP6a based Security Scheme 2
This commit is contained in:
parent
7fa47173b2
commit
0b88785286
@ -8,14 +8,135 @@ Protocol Communication (protocomm) component manages secure sessions and provide
|
|||||||
Following features are available for provisioning :
|
Following features are available for provisioning :
|
||||||
* Communication security at application level -
|
* Communication security at application level -
|
||||||
* protocomm_security0 (no security)
|
* protocomm_security0 (no security)
|
||||||
* protocomm_security1 (curve25519 key exchange + AES-CTR encryption)
|
* protocomm_security1 (Curve25519 key exchange + AES-CTR encryption/decryption)
|
||||||
|
* protocomm_security2 (SRP6a-based key exchange + AES-GCM encryption/decryption)
|
||||||
* Proof-of-possession (support with protocomm_security1 only)
|
* Proof-of-possession (support with protocomm_security1 only)
|
||||||
|
* Salt and Verifier (support with protocomm_security2 only)
|
||||||
|
|
||||||
Protocomm internally uses protobuf (protocol buffers) for secure session establishment. Though users can implement their own security (even without using protobuf). One can even use protocomm without any security layer.
|
Protocomm internally uses protobuf (protocol buffers) for secure session establishment. Though users can implement their own security (even without using protobuf). One can even use protocomm without any security layer.
|
||||||
|
|
||||||
Protocomm provides framework for various transports - WiFi (SoftAP+HTTPD), BLE, console - in which case the handler invocation is automatically taken care of on the device side (see Transport Examples below for code snippets).
|
Protocomm provides framework for various transports - WiFi (SoftAP+HTTPD), BLE, console - in which case the handler invocation is automatically taken care of on the device side (see Transport Examples below for code snippets).
|
||||||
|
|
||||||
Note that the client still needs to establish session (only for protocomm_security1) by performing the two way handshake. See :doc:`provisioning` for more details about the secure handshake logic.
|
Note that the client still needs to establish session (for protocomm_security1 and protocomm_security2) by performing the two way handshake. See :doc:`provisioning` for more details about the secure handshake logic.
|
||||||
|
|
||||||
|
Transport Example (SoftAP + HTTP) with Security 2
|
||||||
|
-------------------------------------------------
|
||||||
|
For sample usage, see :component_file:`wifi_provisioning/src/scheme_softap.c`
|
||||||
|
|
||||||
|
.. highlight:: c
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
/* Endpoint handler to be registered with protocomm.
|
||||||
|
* This simply echoes back the received data. */
|
||||||
|
esp_err_t echo_req_handler (uint32_t session_id,
|
||||||
|
const uint8_t *inbuf, ssize_t inlen,
|
||||||
|
uint8_t **outbuf, ssize_t *outlen,
|
||||||
|
void *priv_data)
|
||||||
|
{
|
||||||
|
/* Session ID may be used for persistence */
|
||||||
|
printf("Session ID : %d", session_id);
|
||||||
|
|
||||||
|
/* Echo back the received data */
|
||||||
|
*outlen = inlen; /* Output data length updated */
|
||||||
|
*outbuf = malloc(inlen); /* This will be deallocated outside */
|
||||||
|
memcpy(*outbuf, inbuf, inlen);
|
||||||
|
|
||||||
|
/* Private data that was passed at the time of endpoint creation */
|
||||||
|
uint32_t *priv = (uint32_t *) priv_data;
|
||||||
|
if (priv) {
|
||||||
|
printf("Private data : %d", *priv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char sec2_salt[] = {0xf7, 0x5f, 0xe2, 0xbe, 0xba, 0x7c, 0x81, 0xcd};
|
||||||
|
static const char sec2_verifier[] = {0xbf, 0x86, 0xce, 0x63, 0x8a, 0xbb, 0x7e, 0x2f, 0x38, 0xa8, 0x19, 0x1b, 0x35,
|
||||||
|
0xc9, 0xe3, 0xbe, 0xc3, 0x2b, 0x45, 0xee, 0x10, 0x74, 0x22, 0x1a, 0x95, 0xbe, 0x62, 0xf7, 0x0c, 0x65, 0x83, 0x50,
|
||||||
|
0x08, 0xef, 0xaf, 0xa5, 0x94, 0x4b, 0xcb, 0xe1, 0xce, 0x59, 0x2a, 0xe8, 0x7b, 0x27, 0xc8, 0x72, 0x26, 0x71, 0xde,
|
||||||
|
0xb2, 0xf2, 0x80, 0x02, 0xdd, 0x11, 0xf0, 0x38, 0x0e, 0x95, 0x25, 0x00, 0xcf, 0xb3, 0x3f, 0xf0, 0x73, 0x2a, 0x25,
|
||||||
|
0x03, 0xe8, 0x51, 0x72, 0xef, 0x6d, 0x3e, 0x14, 0xb9, 0x2e, 0x9f, 0x2a, 0x90, 0x9e, 0x26, 0xb6, 0x3e, 0xc7, 0xe4,
|
||||||
|
0x9f, 0xe3, 0x20, 0xce, 0x28, 0x7c, 0xbf, 0x89, 0x50, 0xc9, 0xb6, 0xec, 0xdd, 0x81, 0x18, 0xf1, 0x1a, 0xd9, 0x7a,
|
||||||
|
0x21, 0x99, 0xf1, 0xee, 0x71, 0x2f, 0xcc, 0x93, 0x16, 0x34, 0x0c, 0x79, 0x46, 0x23, 0xe4, 0x32, 0xec, 0x2d, 0x9e,
|
||||||
|
0x18, 0xa6, 0xb9, 0xbb, 0x0a, 0xcf, 0xc4, 0xa8, 0x32, 0xc0, 0x1c, 0x32, 0xa3, 0x97, 0x66, 0xf8, 0x30, 0xb2, 0xda,
|
||||||
|
0xf9, 0x8d, 0xc3, 0x72, 0x72, 0x5f, 0xe5, 0xee, 0xc3, 0x5c, 0x24, 0xc8, 0xdd, 0x54, 0x49, 0xfc, 0x12, 0x91, 0x81,
|
||||||
|
0x9c, 0xc3, 0xac, 0x64, 0x5e, 0xd6, 0x41, 0x88, 0x2f, 0x23, 0x66, 0xc8, 0xac, 0xb0, 0x35, 0x0b, 0xf6, 0x9c, 0x88,
|
||||||
|
0x6f, 0xac, 0xe1, 0xf4, 0xca, 0xc9, 0x07, 0x04, 0x11, 0xda, 0x90, 0x42, 0xa9, 0xf1, 0x97, 0x3d, 0x94, 0x65, 0xe4,
|
||||||
|
0xfb, 0x52, 0x22, 0x3b, 0x7a, 0x7b, 0x9e, 0xe9, 0xee, 0x1c, 0x44, 0xd0, 0x73, 0x72, 0x2a, 0xca, 0x85, 0x19, 0x4a,
|
||||||
|
0x60, 0xce, 0x0a, 0xc8, 0x7d, 0x57, 0xa4, 0xf8, 0x77, 0x22, 0xc1, 0xa5, 0xfa, 0xfb, 0x7b, 0x91, 0x3b, 0xfe, 0x87,
|
||||||
|
0x5f, 0xfe, 0x05, 0xd2, 0xd6, 0xd3, 0x74, 0xe5, 0x2e, 0x68, 0x79, 0x34, 0x70, 0x40, 0x12, 0xa8, 0xe1, 0xb4, 0x6c,
|
||||||
|
0xaa, 0x46, 0x73, 0xcd, 0x8d, 0x17, 0x72, 0x67, 0x32, 0x42, 0xdc, 0x10, 0xd3, 0x71, 0x7e, 0x8b, 0x00, 0x46, 0x9b,
|
||||||
|
0x0a, 0xe9, 0xb4, 0x0f, 0xeb, 0x70, 0x52, 0xdd, 0x0a, 0x1c, 0x7e, 0x2e, 0xb0, 0x61, 0xa6, 0xe1, 0xa3, 0x34, 0x4b,
|
||||||
|
0x2a, 0x3c, 0xc4, 0x5d, 0x42, 0x05, 0x58, 0x25, 0xd3, 0xca, 0x96, 0x5c, 0xb9, 0x52, 0xf9, 0xe9, 0x80, 0x75, 0x3d,
|
||||||
|
0xc8, 0x9f, 0xc7, 0xb2, 0xaa, 0x95, 0x2e, 0x76, 0xb3, 0xe1, 0x48, 0xc1, 0x0a, 0xa1, 0x0a, 0xe8, 0xaf, 0x41, 0x28,
|
||||||
|
0xd2, 0x16, 0xe1, 0xa6, 0xd0, 0x73, 0x51, 0x73, 0x79, 0x98, 0xd9, 0xb9, 0x00, 0x50, 0xa2, 0x4d, 0x99, 0x18, 0x90,
|
||||||
|
0x70, 0x27, 0xe7, 0x8d, 0x56, 0x45, 0x34, 0x1f, 0xb9, 0x30, 0xda, 0xec, 0x4a, 0x08, 0x27, 0x9f, 0xfa, 0x59, 0x2e,
|
||||||
|
0x36, 0x77, 0x00, 0xe2, 0xb6, 0xeb, 0xd1, 0x56, 0x50, 0x8e};
|
||||||
|
|
||||||
|
/* Example function for launching a protocomm instance over HTTP */
|
||||||
|
protocomm_t *start_pc()
|
||||||
|
{
|
||||||
|
protocomm_t *pc = protocomm_new();
|
||||||
|
|
||||||
|
|
||||||
|
/* Config for protocomm_httpd_start() */
|
||||||
|
protocomm_httpd_config_t pc_config = {
|
||||||
|
.data = {
|
||||||
|
.config = PROTOCOMM_HTTPD_DEFAULT_CONFIG()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Start protocomm server on top of HTTP */
|
||||||
|
protocomm_httpd_start(pc, &pc_config);
|
||||||
|
|
||||||
|
/* Create Security2 params object from salt and verifier. It must be valid
|
||||||
|
* throughout the scope of protocomm endpoint. This need not be static,
|
||||||
|
* ie. could be dynamically allocated and freed at the time of endpoint
|
||||||
|
* removal */
|
||||||
|
const static protocomm_security2_params_t sec2_params = {
|
||||||
|
.salt = (const uint8_t *) salt,
|
||||||
|
.salt_len = sizeof(salt),
|
||||||
|
.verifier = (const uint8_t *) verifier,
|
||||||
|
.verifier_len = sizeof(verifier),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Set security for communication at application level. Just like for
|
||||||
|
* request handlers, setting security creates an endpoint and registers
|
||||||
|
* the handler provided by protocomm_security1. One can similarly use
|
||||||
|
* protocomm_security0. Only one type of security can be set for a
|
||||||
|
* protocomm instance at a time. */
|
||||||
|
protocomm_set_security(pc, "security_endpoint", &protocomm_security2, &sec2_params);
|
||||||
|
|
||||||
|
/* Private data passed to the endpoint must be valid throughout the scope
|
||||||
|
* of protocomm endpoint. This need not be static, ie. could be dynamically
|
||||||
|
* allocated and freed at the time of endpoint removal */
|
||||||
|
static uint32_t priv_data = 1234;
|
||||||
|
|
||||||
|
/* Add a new endpoint for the protocomm instance, identified by a unique name
|
||||||
|
* and register a handler function along with private data to be passed at the
|
||||||
|
* time of handler execution. Multiple endpoints can be added as long as they
|
||||||
|
* are identified by unique names */
|
||||||
|
protocomm_add_endpoint(pc, "echo_req_endpoint",
|
||||||
|
echo_req_handler, (void *) &priv_data);
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example function for stopping a protocomm instance */
|
||||||
|
void stop_pc(protocomm_t *pc)
|
||||||
|
{
|
||||||
|
/* Remove endpoint identified by it's unique name */
|
||||||
|
protocomm_remove_endpoint(pc, "echo_req_endpoint");
|
||||||
|
|
||||||
|
/* Remove security endpoint identified by it's name */
|
||||||
|
protocomm_unset_security(pc, "security_endpoint");
|
||||||
|
|
||||||
|
/* Stop HTTP server */
|
||||||
|
protocomm_httpd_stop(pc);
|
||||||
|
|
||||||
|
/* Delete (deallocate) the protocomm instance */
|
||||||
|
protocomm_delete(pc);
|
||||||
|
}
|
||||||
|
|
||||||
Transport Example (SoftAP + HTTP) with Security 1
|
Transport Example (SoftAP + HTTP) with Security 1
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
@ -65,11 +186,11 @@ For sample usage, see :component_file:`wifi_provisioning/src/scheme_softap.c`
|
|||||||
/* Start protocomm server on top of HTTP */
|
/* Start protocomm server on top of HTTP */
|
||||||
protocomm_httpd_start(pc, &pc_config);
|
protocomm_httpd_start(pc, &pc_config);
|
||||||
|
|
||||||
/* Create Proof of Possession object from pop_string. It must be valid
|
/* Create security1 params object from pop_string. It must be valid
|
||||||
* throughout the scope of protocomm endpoint. This need not be static,
|
* throughout the scope of protocomm endpoint. This need not be static,
|
||||||
* ie. could be dynamically allocated and freed at the time of endpoint
|
* ie. could be dynamically allocated and freed at the time of endpoint
|
||||||
* removal */
|
* removal */
|
||||||
const static protocomm_security_pop_t pop_obj = {
|
const static protocomm_security1_params_t sec1_params = {
|
||||||
.data = (const uint8_t *) strdup(pop_string),
|
.data = (const uint8_t *) strdup(pop_string),
|
||||||
.len = strlen(pop_string)
|
.len = strlen(pop_string)
|
||||||
};
|
};
|
||||||
@ -79,7 +200,7 @@ For sample usage, see :component_file:`wifi_provisioning/src/scheme_softap.c`
|
|||||||
* the handler provided by protocomm_security1. One can similarly use
|
* the handler provided by protocomm_security1. One can similarly use
|
||||||
* protocomm_security0. Only one type of security can be set for a
|
* protocomm_security0. Only one type of security can be set for a
|
||||||
* protocomm instance at a time. */
|
* protocomm instance at a time. */
|
||||||
protocomm_set_security(pc, "security_endpoint", &protocomm_security1, &pop_obj);
|
protocomm_set_security(pc, "security_endpoint", &protocomm_security1, &sec1_params);
|
||||||
|
|
||||||
/* Private data passed to the endpoint must be valid throughout the scope
|
/* Private data passed to the endpoint must be valid throughout the scope
|
||||||
* of protocomm endpoint. This need not be static, ie. could be dynamically
|
* of protocomm endpoint. This need not be static, ie. could be dynamically
|
||||||
|
@ -99,14 +99,18 @@ Application creates a protocomm instance which is mapped to a specific transport
|
|||||||
|
|
||||||
Security Schemes
|
Security Schemes
|
||||||
>>>>>>>>>>>>>>>>
|
>>>>>>>>>>>>>>>>
|
||||||
At present unified provisioning supports two security schemes:
|
At present, unified provisioning supports the following security schemes:
|
||||||
1. Security0 - No security (No encryption)
|
|
||||||
2. Security1 - Curve25519 based key exchange, shared key derivation and AES256-CTR mode encryption of the data. It supports two modes :
|
|
||||||
|
|
||||||
|
1. Security0 - No security (No encryption)
|
||||||
|
2. Security1 - Curve25519-based key exchange, shared key derivation and AES256-CTR mode encryption of the data. It supports two modes :
|
||||||
a. Authorized - Proof of Possession (PoP) string used to authorize session and derive shared key
|
a. Authorized - Proof of Possession (PoP) string used to authorize session and derive shared key
|
||||||
b. No Auth (Null PoP) - Shared key derived through key exchange only
|
b. No Auth (Null PoP) - Shared key derived through key exchange only
|
||||||
|
3. Security2 - SRP6a-based shared key derivation and AES256-GCM mode encryption of the data.
|
||||||
|
|
||||||
Security1 scheme details are shown in the below sequence diagram
|
Security1 Scheme
|
||||||
|
>>>>>>>>>>>>>>>>
|
||||||
|
|
||||||
|
Security1 scheme details are shown in the below sequence diagram -
|
||||||
|
|
||||||
.. seqdiag::
|
.. seqdiag::
|
||||||
:caption: Security1
|
:caption: Security1
|
||||||
@ -140,6 +144,71 @@ Security1 scheme details are shown in the below sequence diagram
|
|||||||
CLIENT -> CLIENT [label = "Verify Device", rightnote = "check (cli_pubkey == aes_ctr_dec(dev_verify...)"];
|
CLIENT -> CLIENT [label = "Verify Device", rightnote = "check (cli_pubkey == aes_ctr_dec(dev_verify...)"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. note:: We shall soon migrate to ``Security2 scheme`` as the default scheme in our examples as it provides enhanced security. This change shall be done once we have our phone apps (Android/iOS) upgraded to handle ``Security2 scheme``.
|
||||||
|
|
||||||
|
Security2 Scheme
|
||||||
|
>>>>>>>>>>>>>>>>
|
||||||
|
|
||||||
|
Security2 scheme is based on the Secure Remote Password (SRP6a) protocol - `RFC 5054 <https://datatracker.ietf.org/doc/html/rfc5054>`_.
|
||||||
|
The protocol requires the Salt and Verifier to be generated beforehand with help of the identifying username ``I`` and the plaintext password ``p``. The Salt and Verifier are then stored on {IDF_TARGET_NAME}.
|
||||||
|
- The password ``p`` and username ``I`` are to be provided to the Phone App (Provisioning entity) by suitable means for example QR code sticker.
|
||||||
|
|
||||||
|
Security2 scheme details are shown in the below sequence diagram -
|
||||||
|
|
||||||
|
.. seqdiag::
|
||||||
|
:caption: Security2
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
seqdiag security2 {
|
||||||
|
activation = none;
|
||||||
|
node_width = 80;
|
||||||
|
node_height = 60;
|
||||||
|
edge_length = 550;
|
||||||
|
span_height = 5;
|
||||||
|
default_shape = roundedbox;
|
||||||
|
default_fontsize = 12;
|
||||||
|
|
||||||
|
CLIENT [label = "Client\n(PhoneApp)"];
|
||||||
|
DEVICE [label = "Device\n(ESP)"];
|
||||||
|
|
||||||
|
=== Security 2 ===
|
||||||
|
CLIENT -> CLIENT [label = "Generate\nKey Pair", rightnote = "a (cli_privkey) = 256 bit random value,
|
||||||
|
A (cli_pubkey) = g^a.
|
||||||
|
g - generator, N - large safe prime,
|
||||||
|
All arithmetic operations are performed in ring of integers modulo N,
|
||||||
|
thus all occurrences like y^z should be read as y^z modulo N."];
|
||||||
|
CLIENT -> DEVICE [label = "SessionCmd0(cli_pubkey A, username I)"];
|
||||||
|
DEVICE -> DEVICE [label = "Obtain\n Salt and Verifier", leftnote = "Obtain salt and verifier stored on esp
|
||||||
|
Salt s = 256 bit random value,
|
||||||
|
Verifier v = g^x where x = H(s | I | p)"];
|
||||||
|
DEVICE -> DEVICE [label = "Generate\nKey Pair", leftnote = "b (dev_privkey) = 256 bit random value
|
||||||
|
B(dev_pubkey) = k*v + g^b where k = H(N, g)"];
|
||||||
|
DEVICE -> DEVICE [label = "Shared Key", leftnote = "Shared Key K = H(S) where,
|
||||||
|
S = (A * v^u) ^ b
|
||||||
|
u = H(A, B)"];
|
||||||
|
DEVICE -> CLIENT [label = "SessionResp0(dev_pubkey B, dev_rand)"];
|
||||||
|
CLIENT -> CLIENT [label = "Shared Key", rightnote = "shared_key(K) = H(S) where,
|
||||||
|
S = (B - k*v) ^ (a + ux),
|
||||||
|
u = H(A, B),
|
||||||
|
k = H(N, g),
|
||||||
|
v = g^x,
|
||||||
|
x = H(s | I | p).
|
||||||
|
|
||||||
|
"];
|
||||||
|
CLIENT -> CLIENT [label = "Verification\nToken", rightnote = "client_proof M = H[H(N) XOR H(g) | H(I) | s | A | B | K]"];
|
||||||
|
CLIENT -> DEVICE [label = "SessionCmd1(client_proof M1)"];
|
||||||
|
DEVICE -> DEVICE [label = "Verify Client", leftnote = "device generates M1 = H[H(N) XOR H(g) | H(I) | s | A | B | K]
|
||||||
|
device verifies this M1 with the M1 obtained from Client"];
|
||||||
|
DEVICE -> DEVICE [label = "Verification\nToken", leftnote = "
|
||||||
|
Device generate device_proof M2 = H(A, M, K)"];
|
||||||
|
DEVICE -> DEVICE [label = "Initialization\nVector", leftnote = "dev_rand = gen_16byte_random()
|
||||||
|
This random number is to be used for AES-GCM operation
|
||||||
|
for encryption and decryption of data using the shared secret"];
|
||||||
|
DEVICE -> CLIENT [label = "SessionResp1(device_proof M2, dev_rand)"];
|
||||||
|
CLIENT -> CLIENT [label = "Verify Device", rightnote = "Client calculates device proof M2 as M2 = H(A, M, K)
|
||||||
|
client verifies this M2 with M2 obtained from device"];
|
||||||
|
}
|
||||||
|
|
||||||
Sample Code
|
Sample Code
|
||||||
>>>>>>>>>>>
|
>>>>>>>>>>>
|
||||||
Please refer to :doc:`protocomm` and :doc:`wifi_provisioning` for API guides and code snippets on example usage.
|
Please refer to :doc:`protocomm` and :doc:`wifi_provisioning` for API guides and code snippets on example usage.
|
||||||
|
@ -11,6 +11,7 @@ ESP-IDF 5.0 Migration Guides
|
|||||||
freertos
|
freertos
|
||||||
peripherals
|
peripherals
|
||||||
protocols
|
protocols
|
||||||
|
provisioning
|
||||||
removed-components
|
removed-components
|
||||||
storage
|
storage
|
||||||
system
|
system
|
||||||
|
25
docs/en/migration-guides/provisioning.rst
Normal file
25
docs/en/migration-guides/provisioning.rst
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
Migrate Provisioning to ESP-IDF 5.0
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Protocomm
|
||||||
|
---------
|
||||||
|
|
||||||
|
The :cpp:func:`protocomm_set_security` API now takes a parameter ``sec_params`` as input instead of ``pop`` (deprecated).
|
||||||
|
This parameter should contain the structure (containing the security parameters) as required by the protocol version used.
|
||||||
|
|
||||||
|
For example when using security version 2, the ``sec_params`` parameter should contain the pointer to the structure of type :cpp:type:`protocomm_security2_params_t`.
|
||||||
|
|
||||||
|
Wi-Fi Provisioning
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The :cpp:func:`wifi_prov_mgr_start_provisioning` API now takes a parameter ``wifi_prov_sec_params`` as input instead of ``pop``.
|
||||||
|
This parameter should contain the structure (containing the security parameters) as required by the protocol version used.
|
||||||
|
|
||||||
|
For example when using security version 2, the ``wifi_prov_sec_params`` parameter should contain the pointer to the structure of type :cpp:type:`wifi_prov_security2_params_t`.
|
||||||
|
|
||||||
|
ESP Local Control
|
||||||
|
-----------------
|
||||||
|
The `pop` field in :cpp:type:`esp_local_ctrl_proto_sec_cfg_t` is now deprecated, use ``sec_params`` field instead of ``pop``.
|
||||||
|
This field should contain the structure (containing the security parameters) as required by the protocol version used.
|
||||||
|
|
||||||
|
For example when using security version 2, the ``sec_params`` field should contain pointer to the structure of type :cpp:type:`esp_local_ctrl_security2_params_t`.
|
@ -11,6 +11,7 @@ ESP-IDF 5.0 迁移指南
|
|||||||
freertos
|
freertos
|
||||||
peripherals
|
peripherals
|
||||||
protocols
|
protocols
|
||||||
|
provisioning
|
||||||
removed-components
|
removed-components
|
||||||
storage
|
storage
|
||||||
system
|
system
|
||||||
|
1
docs/zh_CN/migration-guides/provisioning.rst
Normal file
1
docs/zh_CN/migration-guides/provisioning.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../en/migration-guides/provisioning.rst
|
Loading…
x
Reference in New Issue
Block a user