owned this note
owned this note
Published
Linked with GitHub
# Encrypted NRF24L01 Transport Protocol
This memo describes an idea for secure protocol used for communication with NRF24L01 chips in pair with STM32F103 chips.
This "format" used instead of standard TLS (like HTTPS over TLS over TCP/IP) because you can't really afford having these with something like 20K of RAM and 128K of ROM.
NRF devices should turn off the NRF's packet confirmation, configure address size to 3 bytes, switch to default channel and listen on default address.
Communication uses RSA encryption (2048 bit) to exchange AES key (128 bit), the latter then is used to encrypt the messages.
Every device has: a copy of CA root certificate (`ca`), a signed certificate for the device itself (`crt`) and a private key to said certificate (`priv`).
Upon communication, devices verify certificates using `ca`, encrypt data with `crt` of the remote party, and then remote party decrypts the data with `priv`.
# Communication Flow
In ourder to achieve successfull encrypted communication, devices should:
1. Find each other (`0x1 Discover` / `0x2 Hello`)
2. Exchange X509 Certificates (`0x3 Get Certificate` / `0x4 Certificate`)
3. Negotiate on a AES key (`0x5 Key Negotiate` / `0x6 Negotiate Proceed` / `0x7 Key Data` / `0x8 Key Finish` / `0x9 Negotiate Confirm`)
Then they could do requests:
1. Open a connection (`0xA Request`)
2. Approve the connection (`0xB Proceed`)
2. Send the data (`0xC Request Body`)
3. Receive the response (`0xD Response` / `0xE Response Finish` / `0xF Close Request`)
It looks like this:
![NRF24](https://user-images.githubusercontent.com/1666014/54881471-0ede0d00-4e59-11e9-8407-2ea39d425e96.png)
# Packet format
| Kind | Length | Description |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 hash of packet including flags|
| `FLAGS` | 1 byte | Packet Flags |
- The rest of the packet depends on the packet type (`TYP`)
- Packet size is fixed 32 bytes (5 of which is metadata and 27 is payload)
- If `CRC` does not match, the packet should be dropped
#### Packet Flags
| Name | Bits | Description |
| ---- | ------ | ----------- |
| `TYP ` | first 4 bits, 0x0F | Kind of the packet |
| Reserved | Last 4 bits, 0xF0 | Reserved |
# Packet Types
First 4 bits of the packet flags (`TYP`) contain packet type
### `0x1 Discover`
This packet is sent to find devices nearby. By default, NRFs should turn off packet confirmation, switch to default channel and listen to default address.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x1 |
| `V` | 1 byte | Protocol version = 0x01 |
| `DEVICE_ID` | 3 bytes | ID of the current device |
Everyone who receives the packet should respond within specific time window with packet `0x2` Hello. The time to wait is picked randomly.
### `0x2 Hello`
A response to `0x1 Discover` packet. Normally, everyone sho receive the message should respond. If the device is busy, the packet should be ignored.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x2 |
| `DEVICE_ID` | 3 bytes | ID of the current device |
| `CA_CRC ` | 4 bytes | CRC32 of the CA certificate + `DEVICE_ID` |
| `CRT_CRC ` | 4 bytes | CRC32 of the certificate of the device |
| `CRT_SIZ ` | 2 bytes | Size of the certificate of the devices |
| `PRIORITY` | 1 byte | Used-defined priority for the device |
The data specified in `PRIORITY` shouldn't be trusted and must be only used to optimize network.
### `0x3 Get Certificate`
If device chooses to connect to another device, but do not yet have its certificate, it needs to download it first.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x3 |
| `DEVICE_ID` | 3 bytes | ID of the target device |
| `ADDR` | 3 bytes | Random NRF Address |
| `CH` | 1 byte | Random NRF Channel |
After receiving this message, the sender will switch to randomly picked channel (`CH`) and start listening on random address `ADDR` for limited time. This is implemented to reduce traffic issues.
If a target device received this message, it should switch to the channel `CH`, listen on address `ADDR` and respond with `0x4 Certificate` message to the same address `ADDR`. If the target device is busy, it should ignore this message, and the sender will fail with timeout.
### `0x4 Certificate`
If a device received a `0x3 Get Certificate` message it should switch on channel/address required and respond with this message.
A certificate has length up to 1K bytes, so considering limitation of NRF's max packet size (32 bytes), it has to be delivered sequentially.
The number of packets (and lenght of the certificate) is received with `0x2 Hello`. It is calculated by formula (certificate size) / 26 (rounding up).
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x4 |
| `SEQ ` | 1 byte | Number of the packet in the requence |
| `DATA ` | up to 26 bytes | Part of Certificate |
Packets are sent one by one, without confirmation for the receiving end. If a packet is received out of order, or same packet received twice, the receiver should just drop the request and switch to default channel / default address.
If the last packet is received in order, the receiving party should:
1. Ensure the CRC32 of the Certificate matches the one stated in `0x2 Hello`.
2. Verify the Certificate signature doing `x509_crt_verify(certificate, ca)`
3. Extract the device ID from the certificate (`Common Name`)
4. Validate if the device ID matches the one stated in the `0x2 Hello`.
If the certificate is validated, the certificate should be stored in ROM by `DEVICE_ID`. The sender should immeadetely switch back to the default channel / address after sending the last packet out of sequesnce.
### `0x5 Negotiate`
If a device wants to send a request to another device, but they have not yet negotiated on Key Data, the device should send this message first.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x5 |
| `DEVICE_TGT` | 3 bytes | ID of the target device |
| `DEVICE_SRC` | 3 bytes | ID of the current device |
| `ADDR` | 3 bytes | Random NRF Address |
| `CH` | 1 byte | Random NRF Channel |
If the message was received by device with different `DEVICE_ID` from `DEVICE_TGT`, the packet should be ignored. The Certificate of the target device should be obtained first (see `0x3 Get Certificate`).
After receiving this message, the sender will switch to randomly picked channel (`CH`) and start listening on random address `ADDR` for limited time. This is implemented to reduce traffic issues.
If a target device received this message, it should switch to the channel `CH`, listen on address `ADDR`. If the target device is busy, it should ignore this message, and the sender will fail the request with timeout.
If the target device does not have the Certificate of the current device yet, it should respond with `0x3 Get Certificate` on the address `ADDR` channel `CH`.
If the target device has the Certificate of the current device, it should respond back with `0x6 Negotiate Proceed`, inticating that it is ready for the negotiation process.
### `0x6 Negotiate Proceed`
This message is used as a confirmation for beginning of the negotiation process.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x6 |
### `0x7 Key Data`
After receiving the `0x6 Negotiate Proceed` message, the receiver should
1. Generate random AES key (128 bits)
3. Generate a random channel `NG_CH` (1 byte)
4. Generate random address `NG_ADDR` (3 bytes)
Referred as Key Data, then:
1. Encrypt Key Data (20 bytes) with Certificate of the requested device
2. Sign the Key Data (20 bytes) with Private Key of the current device
3. Store the Key Data into RAM
4. Respond with this message on the address `ADDR` / channel `CH` (stated in `0x5 Negotiate`).
Because of length of the encrypted data (256 + 256 bytes), it should be delivered sequentially
(encrypted Key Data and Key Data signature should be concatenated).
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x7 |
| `SEQ ` | 1 byte | Number of the packet in the requence |
| `DATA ` | up to 26 bytes | Part of encrypted Key Data |
Packets are sent one by one, without confirmation for the receiving end. If a packet is received out of order, or same packet received twice, the receiver should just drop the request and switch to default channel / default address.
After sending out this sequence, the sender should follow up with message `0x8 Key Finish`.
### `0x8 Key Finish`
After sending out the `0x7 Key Data` sequence, the sender should follow up with this message.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x8 |
| `SEQ ` | 1 byte | Number of the last (finish) packet in the requence |
| `KEY_SZ ` | 2 bytes | Size of the encrypted key was sent before |
| `SIG_SZ ` | 2 bytes | Size of the encrypted key key signature was sent before |
The data looks like this:
1. `0 .. KEY_SZ - 1` Encrypted Key Data
2. `KEY_SZ .. SIG_SZ - 1` Signature of Encrypted Key Data
If the last packet is received in order, the receiving party should:
1. Verify the Encrypted Key Data with Signature of Encrypted Key Data (using a previously validated certificate of the sender)
2. Decrypt the Encrypted Key Data (using `0x7 Key Data` as a source of data and `0x8 Key Finish` as a source of data length) with Private key of the current device
3. If the RSA key was successfully decrypted, the Key Data should be stored in RAM by `DEVICE_ID`.
4. The received should respond back with `0x9 Negotiate Confirm` to indicate the key has been successfully received.
Regardless if key was delivered or not, bot device should switch back to the default address / channel.
Key Data should be automatically invalidated in reasonable amount of time.
### `0x9 Negotiate Confirm`
This message sent as an indication of successfully received and processed Key Data (`0x7 Key Data` / `0x8 Key Finish`)
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0x9 |
| `DEVICE_TGT` | 3 bytes | ID of the target device |
| `DEVICE_SRC` | 3 bytes | ID of the current device |
If the message was received by device with different `DEVICE_ID` from `DEVICE_TGT`, the packet should be ignored.
This message should only processed in short time window after sending out the Key data to indicate the data was received and the session can be marked as negotiated.
### `0xA Request`
If the two devices have exchanged the Certificates (`0x3 Get Certificate` / `0x4 Certificate`) and negotiated on Key Data (`0x5 Negotiate` / `0x6 Negotiate Confirm` / `0x7 Key Data`) they might open a request exchange.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xA |
| `DEVICE_TGT` | 3 bytes | ID of the target device |
| `DEVICE_SRC` | 3 bytes | ID of the current device |
| `GCM_IV` | 12 bytes | Random `IV` of the GCM for this request |
| `GCM_TAG` | 8 bytes | GCM tag for current requst |
| `REQ_SZ` | 1 bytes | Size of the Request Body divided by 16 (4080 max) |
The request should use a AES-128-GCM algorithm to encrypt the request body with `GCM_IV` and produce a valid `GCM_TAG`.
If the target device has negotiated on Key Data, it should switch to channel `NG_CH`, listen on `NG_ADDR`. If the target device has not yet negotiated with the current device, it should ignore this message.
The currenct device should also switch to channel `NG_CH`, listen on `NG_ADDR` for message `0x9 Proceed` for short time limit. If the device does not receive a `0x9 Proceed` confirmation (or receives one that does not compute) within the time window, it should do several retries (on both defaul and `NG_CH` channels), and then declare the request failed.
### `0xB Proceed`
If a device received a `0xA Request` request, and the current device has negotiated the Key Data with the sender device, the device should respond back with this message .
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xB |
| `STATUS` | 1 byte | Status flag |
| `SEQ` | 1 byte | In case the `STATUS = 0x01 Please retry` contains last aknowledged packet |
Status flag cat take these values:
* `0x00` OK proceed
* `0x01` Please retry (should restart sending out request body starting from `SEQ`)
* `0x02` Busy
After receiving this message, the original sender of `0xA Request` should start sending the request body `0xC Request Body` within specific time window. If the response isn't received in time, unless the device is busy, the sender shourld retry.
### `0xC Request Body`
After receiving the `0xB Proceed` message, the original sender of `0xA Request` should start sending previously encrypted request body within specific time window.
Because of length of the encrypted data (up to 256 bytes), it should be delivered sequentially.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xC |
| `SEQ ` | 1 byte | Number of the packet in the requence |
| `DATA ` | upt to 26 bytes | encrypted data |
Packets are sent one by one, without confirmation for the receiving end. If a packet is received out of order, or same packet received twice, the receiver should just drop the request and switch to default channel / default address.
* If a packet with `SEQ` with value of `0` (zero) is received again, the process of receiving the request should be starter over again.
After delivering the message, the sender should wait on same channel / address either for `0xD Response` or for `0xF Close Request`
### `0xD Response`
If a device received a `0xC Request Body` request, and the current device has negotiated the key with the sender device, the device should:
1. Decrypt the received Request Body with AES-128-GCM / Initial Vector (`GCM_IV`) / Authentication Tag `GCM_TAG`
2. Process the request
3. Encrypt the response with tthe AES-128-GCM / A new random `GCM_IV` / `GCM_TAG`
4. Send this message back as a response sequentually
5. This message should be followed by `0xE Response Finish`
The request response should use a AES-128-GCM algorithm to encrypt the response body with `GCM_IV` and produce a valid `GCM_TAG`.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xD |
| `SEQ ` | 1 byte | Number of the packet in the requence |
| `DATA ` | upt to 26 bytes | encrypted data |
### `0xE Response Finish`
After receiving the `0xD Response` message, the original sender will follow up with this message.
Because of length of the encrypted data (`SZ`, up to 240 bytes), it should be delivered sequentially.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xE |
| `SEQ ` | 1 byte | Number of the packet in the requence |
| `GCM_IV` | 12 bytes | Random `IV` of the GCM for this response |
| `GCM_TAG `| 8 byte | GCM tag of the response |
| `SZ ` | 1 byte | Accurate size of the Response Body divided by 16 (4080 max) |
After delivering the response, the sender should wait for some time window for:
1. Receive `0xA Request` in case the device that made current request wants to make more.
2. Receive `0xF Close Request` in case the device that made current request has no more requests.
In case nothing's received, the device should switch back to the default channel / default address.
### `0xF Close Request`
Issued by the party that made the original request to indicate that there is no future requests and the request-response session should be closed.
| Kind | Length | Description / Value |
| ---- | ------ | ----------- |
| `CRC` | 4 bytes | CRC32 |
| `FLAGS` | 1 byte | `TYP` = 0xF |