1. Today’s topic

Today we build a safe remote-command path:

text
MQTT/CAN/RS-485 transport
→ decoding
→ format and length validation
→ message authentication
→ replay protection
→ operation authorization
→ local constraints validation
→ owner-task queue
→ execution
→ result persistence
→ acknowledgment

The central idea: TLS protects the channel, but a command still needs its own identity, replay protection, and precisely defined execution semantics.

2. Why this matters in your projects

Through MQTT, a device may receive commands to:

text
reconnect EC25
change APN
change MQTT broker
start OTA
change the input filter
restart the device
enable/disable a load

Even with TLS, risks remain:

text
the broker is compromised;
the publisher account is stolen;
an old message is redelivered;
a retained command arrives after reboot;
QoS repeats an operation;
the server sends a command to the wrong device.

CAN CRC and Modbus CRC are not authentication. They protect against accidental errors, not a node capable of constructing a valid frame.

3. Theory

8.3.1 3.1. Command properties

text
Authenticity:
  a trusted sender created the command.
Integrity:
  the fields have not changed.
Freshness:
  this is not replayed old traffic.
Authorization:
  the sender is permitted to perform the operation.
Idempotency:
  redelivery does not create another side effect.
Auditability:
  it is possible to understand what arrived and why it was accepted/rejected.

8.3.2 3.2. Command format

c
typedef enum {
    CMD_NONE = 0,
    CMD_MODEM_RESET,
    CMD_MQTT_RECONNECT,
    CMD_SET_LOG_LEVEL,
    CMD_SET_IR_DUTY,
    CMD_CONFIG_STAGE,
    CMD_CONFIG_COMMIT,
    CMD_OTA_START,
    CMD_SYSTEM_REBOOT,
} command_type_t;
typedef struct {
    uint8_t protocol_version;
    uint32_t issuer_id;
    uint32_t key_id;
    uint32_t target_device_id;
    uint64_t command_id;
    uint64_t sequence;
    uint64_t issued_utc_s;
    uint64_t expires_utc_s;
    uint64_t session_id;
    uint64_t nonce;
    command_type_t type;
    const uint8_t *payload;
    uint16_t payload_length;
    const uint8_t *auth_tag;
    uint16_t auth_tag_length;
} secure_command_view_t;

command_id provides idempotency, sequence provides anti-replay, and target_device_id prevents execution on another device.

8.3.3 3.3. Canonical serialization

Do not sign a C structure as it sits in memory: padding, endianness, enum size, pointers, and uninitialized bytes make the format unstable. Sign a canonical byte array:

text
protocol_version
issuer_id big-endian
key_id big-endian
target_device_id big-endian
command_id big-endian
sequence big-endian
issued_at big-endian
expires_at big-endian
session_id big-endian
nonce big-endian
command_type big-endian
payload_length big-endian
payload bytes

8.3.4 3.4. HMAC-SHA256

text
tag = HMAC-SHA256(key, canonical_command)

Constant-time comparison:

c
static bool constant_time_equal(const uint8_t *a,
                                const uint8_t *b,
                                size_t length)
{
    uint8_t difference = 0;
    for (size_t i = 0; i < length; i++) {
        difference |= (uint8_t)(a[i] ^ b[i]);
    }
    return difference == 0;
}

First check the tag length.

8.3.5 3.5. HMAC or a digital signature

HMAC is faster and simpler, but the device knows a secret capable of creating a MAC. A digital signature lets the device store only a public key while the private signing key stays on the server. A practical arrangement:

text
MQTT server commands:
  server key signature or per-device HMAC
local RS-485:
  per-link HMAC
CAN with a small payload:
  gateway-authenticated session or CAN FD

8.3.6 3.6. Anti-replay

An old command with a valid MAC is still cryptographically valid. sequence/nonce/session is required. Sliding window:

c
typedef struct {
    uint64_t highest_accepted_sequence;
    uint64_t replay_bitmap;
} replay_window_t;

Important: update replay state only after successful MAC/signature verification.

8.3.7 3.7. Idempotency

Prefer state-setting commands:

text
SET_OUTPUT ON
SET_LOG_LEVEL WARN
SET_IR_DUTY 30%

Instead of dangerous actions:

text
TOGGLE_OUTPUT
INCREMENT_COUNTER
PULSE_OUTPUT

Command result cache:

c
typedef enum {
    CMD_RESULT_NONE = 0,
    CMD_RESULT_IN_PROGRESS,
    CMD_RESULT_SUCCESS,
    CMD_RESULT_REJECTED,
    CMD_RESULT_FAILED,
} command_result_code_t;
typedef struct {
    uint64_t command_id;
    command_result_code_t result;
    int32_t error_code;
    uint32_t result_generation;
    uint64_t completed_mono_us;
} command_result_entry_t;

When command_id repeats, do not execute the operation again; return the stored result.

8.3.8 3.8. Authorization and local safety interlocks

Authentication answers “who sent it?”; authorization answers “what may they do?”. Examples of local constraints:

text
IR duty must not exceed the safe maximum;
OTA only on primary power;
factory reset only with a physical button;
GPIO polarity changes only in service mode;
watchdog cannot be fully disabled remotely.

4. Common mistakes

text
1. Treating QoS 2 as a guarantee of one physical action.
2. Using a retained REBOOT command.
3. Signing a C structure.
4. Checking timestamp but not sequence.
5. Updating replay counter before MAC verification.
6. One HMAC key for the entire fleet.
7. Comparing MAC with a variable length.
8. Using memcmp() for a security tag.
9. Executing the command in an MQTT callback.
10. Executing first and saving command_id afterwards.
11. Treating Modbus/CAN CRC as authentication.
12. Allowing all protections to be disabled remotely.

5. Practical assignment for 30-60 minutes

Create SECURE_COMMAND_POLICY.md:

markdown
# Secure command policy
1. Transport security does not replace message authentication.
2. Every command has target_device_id, command_id and sequence.
3. Commands are authenticated before replay state is changed.
4. Imperative MQTT commands are never retained.
5. Duplicate command_id never repeats a side effect.
6. Authorization is checked separately from authentication.
7. Local safety checks may reject an authenticated command.
8. Critical command results are persisted before ACK.
9. Keys are unique per device or verification uses a server public key.
10. Security failures use rate-limited diagnostics.

Core API:

c
typedef enum {
    SEC_CMD_OK = 0,
    SEC_CMD_BAD_FORMAT,
    SEC_CMD_WRONG_TARGET,
    SEC_CMD_UNKNOWN_KEY,
    SEC_CMD_BAD_AUTH,
    SEC_CMD_REPLAY,
    SEC_CMD_EXPIRED,
    SEC_CMD_UNAUTHORIZED,
    SEC_CMD_INVALID_PARAMS,
    SEC_CMD_DUPLICATE,
} secure_command_status_t;
typedef struct {
    uint32_t device_id;
    uint64_t current_session_id;
    uint64_t current_utc_s;
    bool utc_valid;
    replay_window_t replay;
} secure_command_context_t;

Unit tests:

text
valid command;
a payload bit changed;
repeated sequence;
repeated command_id;
old session_id;
expired;
unknown key_id;
unauthorized;
zero MAC length;
payload length exceeds the buffer.

6. Further reading

  • Mbed TLS HMAC/SHA256.
  • ESP-IDF Security Overview.
  • MQTT QoS/retained semantics.

Brief recap

text
untrusted transport payload
→ bounded decoder
→ canonical representation
→ HMAC/signature verification
→ target/session check
→ anti-replay
→ authorization
→ local safety validation
→ idempotency lookup
→ owner-task execution
→ persistent result
→ authenticated response

Exercise

A valid command is delivered twice after its acknowledgment is lost. Define result lookup and explain why transport QoS cannot prove one physical side effect.

Self-check criteria: Describe the durable execution/result boundary and interrupted-operation recovery. Do not promise exactly-once physical effects merely from a cache or QoS.

Show the supplied answer

Authenticate and validate the command, identify command_id, and consult retained result/state before execution. Return the existing result for a completed duplicate. Delivery semantics alone do not establish one physical execution.

Exercise

A valid MAC packet targets another device, repeats an old sequence, or requests a locally unsafe operation. Define the validation order.

Self-check criteria: Keep authentication, freshness, authorization, and local safety distinct; record bounded rejection evidence without keys.

Show the supplied answer

Decode within bounds, verify canonical authentication, then check target/session, replay policy, authorization, and local safety before owner-task execution. An invalid MAC must not advance replay state; rejected commands must not cause the forbidden side effect.