feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
/* Host tests only. In your firmware use its own Mbed TLS configuration. */
|
||||
#define MBEDTLS_MD_C
|
||||
#define MBEDTLS_SHA256_C
|
||||
#define MBEDTLS_BASE64_C
|
||||
#define MBEDTLS_PLATFORM_C
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "vl_protocol.h"
|
||||
#include "vectors.generated.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int test_rng(void *context, uint8_t *out, size_t n) {
|
||||
(void)context;
|
||||
for (size_t i = 0; i < n; i++) out[i] = (uint8_t)i;
|
||||
return 0; /* Fixed bytes are ONLY for this offline test. */
|
||||
}
|
||||
static int failed_rng(void *context, uint8_t *out, size_t n) {
|
||||
(void)context; (void)out; (void)n; return -1;
|
||||
}
|
||||
static int read_abc(void *context, uint8_t *out, size_t cap, size_t *n) {
|
||||
size_t *offset = context;
|
||||
if (!cap) return -1;
|
||||
*n = *offset < 3 ? 1 : 0;
|
||||
if (*n) out[0] = (uint8_t)"abc"[(*offset)++];
|
||||
return 0;
|
||||
}
|
||||
static int read_error(void *context, uint8_t *out, size_t cap, size_t *n) {
|
||||
(void)context; (void)out; (void)cap; (void)n; return -1;
|
||||
}
|
||||
int main(void) {
|
||||
uint8_t key[32], decoded[4096], frame[2050];
|
||||
char hash[65], signature[65], nonce[23], uuid[37];
|
||||
size_t n;
|
||||
assert(vl_key_from_hex(TEST_KEY, key) == 0);
|
||||
assert(vl_key_from_hex("short", key) != 0);
|
||||
assert(vl_key_from_hex(NULL, key) != 0);
|
||||
assert(vl_sha256_hex("{}", 2, hash) == 0 && !strcmp(hash, BODY_SHA));
|
||||
assert(vl_sha256_hex(NULL, 0, hash) == 0 && !strcmp(hash, EMPTY_SHA));
|
||||
assert(vl_http_sign(key, "POST", "/api/v1/device/ble-state", "KP01-020000260914",
|
||||
"1", "1789372800000", "AAECAwQFBgcICQoLDA0ODw", "12345678-1234-4234-8234-123456789abc",
|
||||
"{}", 2, signature) == 0 && !strcmp(signature, HTTP_SIGNATURE));
|
||||
assert(vl_http_sign(key, "POST", "/api/v1/device/ble-state", "KP01-020000260914",
|
||||
"1", "1789372800000", "AAECAwQFBgcICQoLDA0ODw", "12345678-1234-4234-8234-123456789abc",
|
||||
"{ }", 3, signature) == 0 && strcmp(signature, HTTP_SIGNATURE));
|
||||
assert(vl_http_sign(key, "POST", "/bad\npath", "device", "1", "1", "nonce", "id",
|
||||
"{}", 2, signature) != 0);
|
||||
assert(vl_nonce(test_rng, NULL, nonce) == 0 && !strcmp(nonce, "AAECAwQFBgcICQoLDA0ODw"));
|
||||
assert(vl_uuid4(test_rng, NULL, uuid) == 0 && !strcmp(uuid, "00010203-0405-4607-8809-0a0b0c0d0e0f"));
|
||||
assert(vl_nonce(failed_rng, NULL, nonce) != 0 && nonce[0] == 0);
|
||||
assert(vl_uuid4(failed_rng, NULL, uuid) != 0 && uuid[0] == 0);
|
||||
const char *json = "{\"cmd\":\"get_info\"}";
|
||||
assert(vl_ble_pack(json, strlen(json), frame, sizeof(frame), &n) == 0);
|
||||
assert(n == 20 && frame[0] == 18 && frame[1] == 0 && !memcmp(frame + 2, json, 18));
|
||||
assert(vl_ble_pack(json, strlen(json), frame, 19, &n) != 0 && n == 0);
|
||||
memset(decoded, 'a', 2049);
|
||||
assert(vl_ble_pack(decoded, 2048, frame, sizeof(frame), &n) == 0 && n == 2050);
|
||||
assert(frame[0] == 0 && frame[1] == 8);
|
||||
assert(vl_ble_pack(decoded, 2049, frame, sizeof(frame), &n) != 0);
|
||||
assert(vl_ble_pack("\xe4\xbd\xa0", 3, frame, sizeof(frame), &n) == 0 && frame[0] == 3);
|
||||
mbedtls_md_context_t sha;
|
||||
assert(vl_sha256_begin(&sha) == 0);
|
||||
assert(vl_sha256_update(&sha, "a", 1) == 0);
|
||||
assert(vl_sha256_update(&sha, "bc", 2) == 0);
|
||||
assert(vl_sha256_finish(&sha, hash) == 0 && !strcmp(hash, ABC_SHA));
|
||||
mbedtls_md_free(&sha);
|
||||
size_t offset = 0;
|
||||
assert(vl_ota_verify(read_abc, &offset, 3, ABC_SHA) == 0);
|
||||
offset = 0;
|
||||
assert(vl_ota_verify(read_abc, &offset, 2, ABC_SHA) != 0);
|
||||
offset = 0;
|
||||
assert(vl_ota_verify(read_abc, &offset, 4, ABC_SHA) != 0);
|
||||
offset = 0;
|
||||
assert(vl_ota_verify(read_abc, &offset, 3, EMPTY_SHA) != 0);
|
||||
assert(vl_ota_verify(read_error, NULL, 3, ABC_SHA) != 0);
|
||||
puts("PASS: HTTP signature, SHA256/OTA streaming, UUID/nonce, BLE framing and error cases");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "vl_protocol.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include <string.h>
|
||||
|
||||
static int hex_digit(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
static void to_hex(const uint8_t *in, size_t n, char *out) {
|
||||
const char *hex = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[i * 2] = hex[in[i] >> 4];
|
||||
out[i * 2 + 1] = hex[in[i] & 15];
|
||||
}
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
int vl_key_from_hex(const char *hex, uint8_t key[32]) {
|
||||
if (!hex || !key || strlen(hex) != 64) return -1;
|
||||
for (size_t i = 0; i < 64; i++) if (hex_digit(hex[i]) < 0) return -1;
|
||||
for (size_t i = 0; i < 32; i++)
|
||||
key[i] = (uint8_t)((hex_digit(hex[i * 2]) << 4) | hex_digit(hex[i * 2 + 1]));
|
||||
return 0;
|
||||
}
|
||||
int vl_nonce(vl_rng_fn rng, void *context, char out[23]) {
|
||||
uint8_t random[16], base64[25];
|
||||
size_t n = 0;
|
||||
if (!rng || !out) return -1;
|
||||
out[0] = '\0';
|
||||
if (rng(context, random, sizeof(random)) != 0) return -1;
|
||||
if (mbedtls_base64_encode(base64, sizeof(base64), &n, random, sizeof(random))) return -1;
|
||||
if (n != 24) return -1;
|
||||
for (size_t i = 0; i < 22; i++)
|
||||
out[i] = base64[i] == '+' ? '-' : base64[i] == '/' ? '_' : (char)base64[i];
|
||||
out[22] = '\0';
|
||||
return 0;
|
||||
}
|
||||
int vl_uuid4(vl_rng_fn rng, void *context, char out[37]) {
|
||||
uint8_t random[16];
|
||||
char hex[33];
|
||||
size_t p = 0;
|
||||
if (!rng || !out) return -1;
|
||||
out[0] = '\0';
|
||||
if (rng(context, random, sizeof(random)) != 0) return -1;
|
||||
random[6] = (uint8_t)((random[6] & 15) | 0x40);
|
||||
random[8] = (uint8_t)((random[8] & 63) | 0x80);
|
||||
to_hex(random, 16, hex);
|
||||
for (size_t i = 0; i < 32; i++) {
|
||||
if (i == 8 || i == 12 || i == 16 || i == 20) out[p++] = '-';
|
||||
out[p++] = hex[i];
|
||||
}
|
||||
out[p] = '\0';
|
||||
return 0;
|
||||
}
|
||||
int vl_sha256_begin(mbedtls_md_context_t *context) {
|
||||
if (!context) return -1;
|
||||
mbedtls_md_init(context);
|
||||
const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
if (!info) return -1;
|
||||
int rc = mbedtls_md_setup(context, info, 0);
|
||||
if (rc == 0) rc = mbedtls_md_starts(context);
|
||||
return rc;
|
||||
}
|
||||
int vl_sha256_update(mbedtls_md_context_t *context, const void *bytes, size_t n) {
|
||||
if (!context || (!bytes && n)) return -1;
|
||||
return n ? mbedtls_md_update(context, bytes, n) : 0;
|
||||
}
|
||||
int vl_sha256_finish(mbedtls_md_context_t *context, char out[65]) {
|
||||
uint8_t digest[32];
|
||||
if (!context || !out) return -1;
|
||||
out[0] = '\0';
|
||||
int rc = mbedtls_md_finish(context, digest);
|
||||
if (rc == 0) to_hex(digest, 32, out);
|
||||
return rc;
|
||||
}
|
||||
int vl_sha256_hex(const void *bytes, size_t length, char out[65]) {
|
||||
if (!out || (!bytes && length)) return -1;
|
||||
out[0] = '\0';
|
||||
mbedtls_md_context_t context;
|
||||
int rc = vl_sha256_begin(&context);
|
||||
if (rc == 0) rc = vl_sha256_update(&context, bytes, length);
|
||||
if (rc == 0) rc = vl_sha256_finish(&context, out);
|
||||
mbedtls_md_free(&context);
|
||||
return rc;
|
||||
}
|
||||
/* Hash the exact LF-joined fields without building a large intermediate string. */
|
||||
static int hmac_fields(const uint8_t key[32], const char **fields,
|
||||
size_t count, char out[65]) {
|
||||
if (!key || !fields || !out) return -1;
|
||||
out[0] = '\0';
|
||||
for (size_t i = 0; i < count; i++)
|
||||
if (!fields[i] || strchr(fields[i], '\n') || strchr(fields[i], '\r')) return -1;
|
||||
const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
if (!info) return -1;
|
||||
mbedtls_md_context_t context;
|
||||
uint8_t digest[32];
|
||||
mbedtls_md_init(&context);
|
||||
int rc = mbedtls_md_setup(&context, info, 1);
|
||||
if (rc == 0) rc = mbedtls_md_hmac_starts(&context, key, 32);
|
||||
for (size_t i = 0; rc == 0 && i < count; i++) {
|
||||
if (i) rc = mbedtls_md_hmac_update(&context, (const uint8_t *)"\n", 1);
|
||||
if (rc == 0) rc = mbedtls_md_hmac_update(&context, (const uint8_t *)fields[i], strlen(fields[i]));
|
||||
}
|
||||
if (rc == 0) rc = mbedtls_md_hmac_finish(&context, digest);
|
||||
mbedtls_md_free(&context);
|
||||
if (rc == 0) to_hex(digest, 32, out);
|
||||
return rc;
|
||||
}
|
||||
int vl_http_sign(const uint8_t key[32], const char *method, const char *path,
|
||||
const char *device_id, const char *key_version,
|
||||
const char *timestamp_ms, const char *nonce,
|
||||
const char *request_id, const void *body, size_t body_length,
|
||||
char signature_hex[65]) {
|
||||
if (!signature_hex) return -1;
|
||||
signature_hex[0] = '\0';
|
||||
if (!method || strcmp(method, "POST") || !path || path[0] != '/') return -1;
|
||||
char body_sha[65];
|
||||
if (vl_sha256_hex(body, body_length, body_sha)) return -1;
|
||||
const char *fields[] = {"DEVREQ1", method, path, device_id, key_version,
|
||||
timestamp_ms, nonce, request_id, body_sha};
|
||||
return hmac_fields(key, fields, 9, signature_hex);
|
||||
}
|
||||
int vl_ble_pack(const void *json_utf8, size_t length,
|
||||
uint8_t *frame, size_t capacity, size_t *frame_length) {
|
||||
if (!frame_length) return -1;
|
||||
*frame_length = 0;
|
||||
if (!json_utf8 || !frame || length == 0 || length > 2048 || capacity < length + 2) return -1;
|
||||
frame[0] = (uint8_t)(length & 255);
|
||||
frame[1] = (uint8_t)(length >> 8);
|
||||
memcpy(frame + 2, json_utf8, length);
|
||||
*frame_length = length + 2;
|
||||
return 0;
|
||||
}
|
||||
int vl_ota_verify(vl_read_fn read, void *context, uint64_t expected_size,
|
||||
const char *expected_sha256) {
|
||||
uint8_t buffer[1024], expected[32], actual[32];
|
||||
char hash[65];
|
||||
uint64_t total = 0;
|
||||
if (!read || !expected_size || vl_key_from_hex(expected_sha256, expected)) return -1;
|
||||
mbedtls_md_context_t sha;
|
||||
int rc = vl_sha256_begin(&sha);
|
||||
while (rc == 0) {
|
||||
size_t n = 0;
|
||||
if (read(context, buffer, sizeof(buffer), &n) != 0 || n > sizeof(buffer)) { rc = -1; break; }
|
||||
if (n == 0) break;
|
||||
if ((uint64_t)n > expected_size - total) { rc = -1; break; }
|
||||
total += n;
|
||||
rc = vl_sha256_update(&sha, buffer, n);
|
||||
}
|
||||
if (rc == 0 && total != expected_size) rc = -1;
|
||||
if (rc == 0) rc = vl_sha256_finish(&sha, hash);
|
||||
mbedtls_md_free(&sha);
|
||||
if (rc != 0 || vl_key_from_hex(hash, actual)) return -1;
|
||||
return memcmp(actual, expected, 32) == 0 ? 0 : -1;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef VL_PROTOCOL_H
|
||||
#define VL_PROTOCOL_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "mbedtls/md.h"
|
||||
|
||||
/* All functions return 0 on success, nonzero on failure. Not board firmware.
|
||||
* String outputs include NUL, binary outputs do not. Never transmit the NUL.
|
||||
* Caller owns buffers; inputs/outputs must not overlap. */
|
||||
int vl_key_from_hex(const char *hex, uint8_t key[32]);
|
||||
/* rng must fill ALL n bytes from the hardware/OS secure RNG; return 0 or fail. */
|
||||
typedef int (*vl_rng_fn)(void *context, uint8_t *bytes, size_t n);
|
||||
int vl_nonce(vl_rng_fn rng, void *context, char out[23]);
|
||||
int vl_uuid4(vl_rng_fn rng, void *context, char out[37]);
|
||||
int vl_sha256_hex(const void *bytes, size_t length, char out[65]);
|
||||
int vl_http_sign(const uint8_t key[32], const char *method, const char *path,
|
||||
const char *device_id, const char *key_version,
|
||||
const char *timestamp_ms, const char *nonce,
|
||||
const char *request_id, const void *body, size_t body_length,
|
||||
char signature_hex[65]);
|
||||
int vl_ble_pack(const void *json_utf8, size_t length,
|
||||
uint8_t *frame, size_t capacity, size_t *frame_length);
|
||||
/* Stream OTA bytes with bounded memory. Always call mbedtls_md_free after
|
||||
* begin, even if an update/finish fails. Do not begin twice on a live context. */
|
||||
int vl_sha256_begin(mbedtls_md_context_t *context);
|
||||
int vl_sha256_update(mbedtls_md_context_t *context, const void *bytes, size_t n);
|
||||
int vl_sha256_finish(mbedtls_md_context_t *context, char out[65]);
|
||||
/* Read already downloaded bytes (e.g. flash/file): 0=success, *n=0 means EOF.
|
||||
* On each call write at most capacity bytes; return nonzero on I/O failure. */
|
||||
typedef int (*vl_read_fn)(void *context, uint8_t *out, size_t capacity, size_t *n);
|
||||
int vl_ota_verify(vl_read_fn read, void *context, uint64_t expected_size,
|
||||
const char *expected_sha256);
|
||||
#endif
|
||||
Reference in New Issue
Block a user