Restructure and improve

This commit is contained in:
2026-03-01 20:12:02 +01:00
parent 593b9e1409
commit aba48e463f
14 changed files with 887 additions and 1050 deletions

View File

@@ -1,3 +1,6 @@
idf_component_register(SRCS "main.cpp"
idf_component_register(SRCS "main.c"
"wifi.c"
"atecc608a.c"
"ssh_client.c"
INCLUDE_DIRS "."
REQUIRES mbedtls esp-cryptoauthlib esp_http_server esp_wifi nvs_flash json esp_timer efuse)
REQUIRES mbedtls esp-cryptoauthlib esp_wifi nvs_flash driver)

View File

@@ -1,4 +1,4 @@
menu "Wi-Fi Credentials"
menu "KeyPi"
config WIFI_SSID
string "Wi-Fi SSID"
@@ -12,4 +12,34 @@ config WIFI_PASSWORD
help
Password for the Wi-Fi network.
config SSH_HOSTNAME
string "SSH Server Hostname or IP"
default "192.168.1.1"
help
Hostname or IP address of the SSH server (door controller).
config SSH_PORT
int "SSH Server Port"
default 22
help
TCP port of the SSH server.
config SSH_USERNAME
string "SSH Username"
default "pi"
help
Username to authenticate with on the SSH server.
config SSH_OPEN_COMMAND
string "SSH Open Command"
default "door open"
help
Shell command to execute on the SSH server to open the door.
config SSH_LOCK_COMMAND
string "SSH Lock Command"
default "door lock"
help
Shell command to execute on the SSH server to lock the door.
endmenu

View File

@@ -1,169 +0,0 @@
#ifndef TANG_SERVER_H
#define TANG_SERVER_H
#include "sdkconfig.h"
#include <esp_event.h>
#include <esp_http_server.h>
#include <esp_log.h>
#include <esp_task_wdt.h>
#include <esp_wifi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <string>
static const char *TAG = "TangServer";
// Include core components
#include "atecc608a.h"
#include "zk_auth.h"
#include "zk_handlers.h"
// --- Configuration ---
const char *wifi_ssid = CONFIG_WIFI_SSID;
const char *wifi_password = CONFIG_WIFI_PASSWORD;
// --- Global State ---
bool unlocked = false; // Start inactive until provisioned and authenticated
httpd_handle_t server_http = NULL;
ZKAuth zk_auth; // Zero-Knowledge Authentication
// WiFi event group
static EventGroupHandle_t wifi_event_group;
const int WIFI_CONNECTED_BIT = BIT0;
// --- WiFi Event Handler ---
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi connecting...");
} else if (event_base == WIFI_EVENT &&
event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi disconnected, reconnecting...");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "WiFi connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
}
}
// --- WiFi Setup ---
void setup_wifi() {
// Initialize event group
wifi_event_group = xEventGroupCreate();
// Initialize network interface
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// Set hostname
ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, "esp-tang-lol"));
// Initialize WiFi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// Register event handlers
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL));
// Configure WiFi
wifi_config_t wifi_config = {};
if (strlen(wifi_ssid) > 0) {
strncpy((char *)wifi_config.sta.ssid, wifi_ssid,
sizeof(wifi_config.sta.ssid));
strncpy((char *)wifi_config.sta.password, wifi_password,
sizeof(wifi_config.sta.password));
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_ssid);
} else {
ESP_LOGI(TAG, "No WiFi SSID configured");
}
}
// --- Initial Setup ---
bool perform_initial_setup() {
ESP_LOGI(TAG, "=======================================================");
ESP_LOGI(TAG, "Setup complete! Device is ready to use");
ESP_LOGI(TAG, "NOTE: Exchange key stored unencrypted for prototyping");
ESP_LOGI(TAG, "=======================================================");
return true;
}
// --- Setup HTTP Server Routes ---
httpd_handle_t setup_http_server() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.lru_purge_enable = true;
config.stack_size = 8192;
config.max_uri_handlers = 16;
httpd_handle_t server = NULL;
if (httpd_start(&server, &config) == ESP_OK) {
register_zk_handlers(server);
ESP_LOGI(TAG, "HTTP server listening on port 80");
} else {
ESP_LOGE(TAG, "Failed to start HTTP server");
}
return server;
}
// --- Main Setup ---
void setup() {
// Initialize NVS (required before any storage operations)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS initialized");
if (atecc608B_init()) {
atecc608B_print_config();
} else {
ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed");
}
// Initialize Zero-Knowledge Authentication
ESP_LOGI(TAG, "Initializing Zero-Knowledge Authentication...");
if (zk_auth.init()) {
ESP_LOGI(TAG, "ZK Auth initialized successfully");
} else {
ESP_LOGW(TAG, "ZK Auth initialization failed");
}
setup_wifi();
server_http = setup_http_server();
if (server_http) {
ESP_LOGI(TAG, "HTTP server listening on port 80");
ESP_LOGI(TAG, " - ZK Auth UI: http://<ip>/");
ESP_LOGI(TAG, " - Tang Server: http://<ip>/adv");
}
}
// --- Main Loop ---
void loop() {
// Just delay - HTTP server handles requests in its own task
vTaskDelay(pdMS_TO_TICKS(1000));
}
#endif // TANG_SERVER_H

103
main/atecc608a.c Normal file
View File

@@ -0,0 +1,103 @@
#include "atecc608a.h"
#include "sdkconfig.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
static const char *TAG = "atecc608b";
/* -------------------------------------------------------------------------
* Module-private state
* ---------------------------------------------------------------------- */
static ATCAIfaceCfg s_atecc_cfg;
static uint8_t s_config_data[128];
static bool s_config_valid = false;
/* -------------------------------------------------------------------------
* Internal helpers
* ---------------------------------------------------------------------- */
static bool read_config_zone(void)
{
ATCA_STATUS status = atcab_read_config_zone(s_config_data);
if (status != ATCA_SUCCESS) {
ESP_LOGI(TAG, "Failed to read config zone (0x%02X) check wiring", status);
return false;
}
s_config_valid = true;
return true;
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
bool atecc608B_init(void)
{
ESP_LOGI(TAG, "Initialising ATECC608B...");
s_atecc_cfg.iface_type = ATCA_I2C_IFACE;
s_atecc_cfg.devtype = ATECC608B;
s_atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS;
s_atecc_cfg.atcai2c.bus = 0;
s_atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE;
s_atecc_cfg.wake_delay = 1500;
s_atecc_cfg.rx_retries = 20;
ESP_LOGI(TAG, "SDA=GPIO%d SCL=GPIO%d addr=0x%02X (7-bit 0x%02X)",
CONFIG_ATCA_I2C_SDA_PIN, CONFIG_ATCA_I2C_SCL_PIN,
CONFIG_ATCA_I2C_ADDRESS, CONFIG_ATCA_I2C_ADDRESS >> 1);
ATCA_STATUS status = atcab_init(&s_atecc_cfg);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_init failed: 0x%02X", status);
return false;
}
vTaskDelay(pdMS_TO_TICKS(100));
status = atcab_wakeup();
if (status != ATCA_SUCCESS) {
ESP_LOGW(TAG, "Wake returned 0x%02X (may be normal)", status);
}
atcab_idle();
vTaskDelay(pdMS_TO_TICKS(50));
ESP_LOGI(TAG, "ATECC608B initialised");
return true;
}
void atecc608B_print_config(void)
{
ESP_LOGI(TAG, "=== ATECC608B Configuration Zone ===");
uint8_t serial[9];
ATCA_STATUS status = atcab_read_serial_number(serial);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG, "Failed to read serial number (0x%02X) check wiring", status);
return;
}
if (!read_config_zone()) {
return;
}
uint8_t *c = s_config_data;
for (int i = 0; i < 128; i++) {
if (i % 16 == 0) printf("\n0x%02X: ", i);
printf("%02X ", c[i]);
}
printf("\n\n");
ESP_LOGI(TAG, "=== End of ATECC608B Configuration ===");
}
void atecc608B_release(void)
{
atcab_release();
}

View File

@@ -2,417 +2,23 @@
#define ATECC608B_H
#include "cryptoauthlib.h"
#include "sdkconfig.h"
#include <cJSON.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static const char *TAG_ATECC = "ATECC608B";
// ATECC608B Configuration
ATCAIfaceCfg atecc_cfg;
bool atecc608B_init() {
ESP_LOGI(TAG_ATECC, "\n=== ATECC608B Initialization ===");
// Configure cryptoauthlib for ATECC608B - it will handle I2C initialization
atecc_cfg.iface_type = ATCA_I2C_IFACE;
atecc_cfg.devtype = ATECC608B;
atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS;
atecc_cfg.atcai2c.bus = 0; // I2C bus number
atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE;
atecc_cfg.wake_delay = 1500;
atecc_cfg.rx_retries = 20;
ESP_LOGI(TAG_ATECC,
"Configuring ATECC608B: SDA=GPIO%d, SCL=GPIO%d, Address=0x%02X "
"(7-bit: 0x%02X)",
CONFIG_ATCA_I2C_SDA_PIN, CONFIG_ATCA_I2C_SCL_PIN,
CONFIG_ATCA_I2C_ADDRESS, CONFIG_ATCA_I2C_ADDRESS >> 1);
// Initialize cryptoauthlib (it will initialize I2C internally)
ATCA_STATUS status = atcab_init(&atecc_cfg);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_ATECC, "ERROR: atcab_init failed with status 0x%02X", status);
return false;
}
ESP_LOGI(TAG_ATECC, "ATECC608B initialized successfully");
// Give the device a moment to stabilize
vTaskDelay(pdMS_TO_TICKS(100));
// Wake the device and test communication
status = atcab_wakeup();
if (status != ATCA_SUCCESS) {
ESP_LOGW(TAG_ATECC, "WARNING: Wake command returned status 0x%02X", status);
}
// Put device to idle
atcab_idle();
vTaskDelay(pdMS_TO_TICKS(50));
return true;
}
// Global storage for config zone data
uint8_t g_atecc_config_data[128] = {0};
bool g_atecc_config_valid = false;
#include <stdbool.h>
/**
* Read ATECC608B configuration zone into global buffer
* Initialise the ATECC608B over I2C.
* Returns true on success.
*/
bool atecc608B_read_config() {
ESP_LOGI(TAG_ATECC, "Reading ATECC608B configuration zone...");
ATCA_STATUS status = atcab_read_config_zone(g_atecc_config_data);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read config zone, status 0x%02X",
status);
// Try reading it in blocks as a workaround
ESP_LOGI(TAG_ATECC, "Attempting to read config in 4-byte blocks...");
bool read_success = true;
for (uint8_t block = 0; block < 32; block++) {
status = atcab_read_zone(ATCA_ZONE_CONFIG, 0, block, 0,
&g_atecc_config_data[block * 4], 4);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read block %d, status 0x%02X",
block, status);
read_success = false;
break;
}
}
if (!read_success) {
ESP_LOGE(TAG_ATECC, "ERROR: Could not read configuration zone");
g_atecc_config_valid = false;
return false;
}
ESP_LOGI(TAG_ATECC, "Successfully read config in blocks");
}
g_atecc_config_valid = true;
return true;
}
bool atecc608B_init(void);
/**
* Print ATECC608B configuration zone to console with detailed subzone breakdown
* According to ATECC608B Table 2-4
* Read the full configuration zone and print it to the console.
* Useful for first-time setup diagnostics.
*/
void atecc608B_print_config() {
ESP_LOGI(TAG_ATECC, "\n=== ATECC608B Configuration Zone ===");
// First, try to read the serial number as a simple communication test
uint8_t serial_number[9];
ATCA_STATUS status = atcab_read_serial_number(serial_number);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read serial number, status 0x%02X",
status);
ESP_LOGE(TAG_ATECC, "This might indicate a communication or wiring issue.");
return;
}
// Read configuration zone into global buffer
if (!atecc608B_read_config()) {
return;
}
uint8_t *config_data = g_atecc_config_data;
// Print complete hex dump first
ESP_LOGI(TAG_ATECC, "\n--- Complete Configuration Zone (128 bytes) ---");
for (int i = 0; i < 128; i++) {
if (i % 16 == 0) {
printf("\n0x%02X: ", i);
}
printf("%02X ", config_data[i]);
}
printf("\n");
// Print detailed subzone breakdown according to Table 2-4
ESP_LOGI(TAG_ATECC, "\n--- Subzone Breakdown (Table 2-4) ---");
// Bytes 0-3: Serial Number[0:3]
printf("\n[Bytes 0-3] Serial Number[0:3]: ");
for (int i = 0; i < 4; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Bytes 4-7: Revision Number
printf("[Bytes 4-7] Revision Number: ");
for (int i = 4; i < 8; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Bytes 8-12: Serial Number[4:8]
printf("[Bytes 8-12] Serial Number[4:8]: ");
for (int i = 8; i < 13; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Full Serial Number
printf(" --> Complete Serial Number: ");
for (int i = 0; i < 4; i++)
printf("%02X", config_data[i]);
for (int i = 8; i < 13; i++)
printf("%02X", config_data[i]);
printf("\n");
// Byte 13: Reserved
printf("[Byte 13] Reserved: %02X\n", config_data[13]);
// Byte 14: I2C_Enable
printf("[Byte 14] I2C_Enable: %02X\n", config_data[14]);
// Byte 15: Reserved
printf("[Byte 15] Reserved: %02X\n", config_data[15]);
// Byte 16: I2C_Address
printf("[Byte 16] I2C_Address: 0x%02X (7-bit: 0x%02X)\n", config_data[16],
config_data[16] >> 1);
// Byte 17: Reserved
printf("[Byte 17] Reserved: %02X\n", config_data[17]);
// Byte 18: OTPmode
printf("[Byte 18] OTPmode: 0x%02X\n", config_data[18]);
// Byte 19: ChipMode
printf("[Byte 19] ChipMode: 0x%02X ", config_data[19]);
if (config_data[19] & 0x01)
printf("[I2C_UserExtraAdd] ");
if (config_data[19] & 0x02)
printf("[TTL_Enable] ");
if (config_data[19] & 0x04)
printf("[Watchdog_1.3s] ");
printf("\n");
// Bytes 20-51: SlotConfig[0:15] (16 slots × 2 bytes)
printf("\n[Bytes 20-51] SlotConfig[0:15]:\n");
for (int slot = 0; slot < 16; slot++) {
int offset = 20 + (slot * 2);
uint8_t slot_config_low = config_data[offset];
uint8_t slot_config_high = config_data[offset + 1];
uint16_t slot_config = (slot_config_high << 8) | slot_config_low;
printf(" Slot %2d [Bytes %2d-%2d]: 0x%02X 0x%02X (", slot, offset,
offset + 1, slot_config_low, slot_config_high);
// Print 16-bit binary representation
for (int b = 15; b >= 0; b--)
printf("%d", (slot_config >> b) & 1);
printf(")");
bool is_secret = (slot_config & 0x8000) != 0;
bool encrypt_read = (slot_config & 0x4000) != 0;
if (is_secret)
printf(" [Secret]");
if (encrypt_read)
printf(" [EncryptRead]");
printf("\n");
}
// Bytes 52-59: Counter[0]
printf("\n[Bytes 52-59] Counter[0]: ");
for (int i = 52; i < 60; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Bytes 60-67: Counter[1]
printf("[Bytes 60-67] Counter[1]: ");
for (int i = 60; i < 68; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Bytes 68-83: LastKeyUse[0:15]
printf("\n[Bytes 68-83] LastKeyUse[0:15]: ");
for (int i = 68; i < 84; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Byte 84: UserExtra
printf("\n[Byte 84] UserExtra: 0x%02X\n", config_data[84]);
// Byte 85: Selector
printf("[Byte 85] Selector: 0x%02X\n", config_data[85]);
// Byte 86: LockValue (Data/OTP Zone Lock)
printf("[Byte 86] LockValue (Data/OTP): 0x%02X %s\n", config_data[86],
config_data[86] == 0x00 ? "[LOCKED]" : "[UNLOCKED]");
// Byte 87: LockConfig (Config Zone Lock)
printf("[Byte 87] LockConfig: 0x%02X %s\n", config_data[87],
config_data[87] == 0x00 ? "[LOCKED]" : "[UNLOCKED]");
// Bytes 88-89: SlotLocked
printf("\n[Bytes 88-89] SlotLocked: %02X %02X\n", config_data[88],
config_data[89]);
// Bytes 90-91: ChipOptions
printf("[Bytes 90-91] ChipOptions: ");
uint16_t chip_options = (config_data[91] << 8) | config_data[90];
printf("0x%04X\n", chip_options);
// Bytes 92-95: X509format
printf("[Bytes 92-95] X509format: ");
for (int i = 92; i < 96; i++)
printf("%02X ", config_data[i]);
printf("\n");
// Bytes 96-127: KeyConfig[0:15] (16 slots × 2 bytes)
printf("\n[Bytes 96-127] KeyConfig[0:15]:\n");
for (int slot = 0; slot < 16; slot++) {
int offset = 96 + (slot * 2);
uint8_t key_config_low = config_data[offset];
uint8_t key_config_high = config_data[offset + 1];
uint16_t key_config = (key_config_high << 8) | key_config_low;
printf(" Slot %2d [Bytes %2d-%2d]: 0x%02X 0x%02X (", slot, offset,
offset + 1, key_config_low, key_config_high);
// Print 16-bit binary representation
for (int b = 15; b >= 0; b--)
printf("%d", (key_config >> b) & 1);
printf(")");
bool is_private = (key_config & 0x0001) != 0;
if (is_private)
printf(" [Private]");
printf("\n");
}
ESP_LOGI(TAG_ATECC, "\n=== End of ATECC608B Configuration ===\n");
}
void atecc608B_print_config(void);
/**
* Get ATECC608B configuration as JSON string (caller must free the returned
* string)
* Release cryptoauthlib resources.
*/
char *atecc608B_get_config_json() {
if (!g_atecc_config_valid) {
ESP_LOGE(TAG_ATECC,
"Config data not valid. Call atecc608B_print_config() first.");
return NULL;
}
void atecc608B_release(void);
uint8_t *config = g_atecc_config_data;
cJSON *root = cJSON_CreateObject();
// Add raw hex data
char hex_str[385]; // 128 bytes * 3 chars per byte + null
char *ptr = hex_str;
for (int i = 0; i < 128; i++) {
ptr += sprintf(ptr, "%02X ", config[i]);
}
cJSON_AddStringToObject(root, "raw_hex", hex_str);
// Serial Number
char serial_str[19];
sprintf(serial_str, "%02X%02X%02X%02X%02X%02X%02X%02X%02X", config[0],
config[1], config[2], config[3], config[8], config[9], config[10],
config[11], config[12]);
cJSON_AddStringToObject(root, "serial_number", serial_str);
// Revision
char revision_str[12];
sprintf(revision_str, "%02X%02X%02X%02X", config[4], config[5], config[6],
config[7]);
cJSON_AddStringToObject(root, "revision", revision_str);
// I2C settings
cJSON_AddNumberToObject(root, "i2c_enable", config[14]);
cJSON_AddNumberToObject(root, "i2c_address", config[16]);
// Mode settings
cJSON_AddNumberToObject(root, "otp_mode", config[18]);
cJSON_AddNumberToObject(root, "chip_mode", config[19]);
// Lock status
cJSON *locks = cJSON_CreateObject();
cJSON_AddBoolToObject(locks, "config_locked", config[87] == 0x00);
cJSON_AddBoolToObject(locks, "data_otp_locked", config[86] == 0x00);
cJSON_AddItemToObject(root, "locks", locks);
// Slot configurations
cJSON *slots = cJSON_CreateArray();
for (int slot = 0; slot < 16; slot++) {
cJSON *slot_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(slot_obj, "slot", slot);
int slot_config_offset = 20 + (slot * 2);
uint8_t slot_config_low = config[slot_config_offset];
uint8_t slot_config_high = config[slot_config_offset + 1];
uint16_t slot_config = (slot_config_high << 8) | slot_config_low;
cJSON *slot_config_arr = cJSON_CreateArray();
char slot_config_low_str[5];
char slot_config_high_str[5];
sprintf(slot_config_low_str, "0x%02X", slot_config_low);
sprintf(slot_config_high_str, "0x%02X", slot_config_high);
cJSON_AddItemToArray(slot_config_arr,
cJSON_CreateString(slot_config_low_str));
cJSON_AddItemToArray(slot_config_arr,
cJSON_CreateString(slot_config_high_str));
cJSON_AddItemToObject(slot_obj, "slot_config", slot_config_arr);
// Add 16-bit binary representation
char slot_config_bin[18];
for (int b = 0; b < 16; b++) {
slot_config_bin[15 - b] = ((slot_config >> b) & 1) ? '1' : '0';
}
slot_config_bin[16] = '\0';
cJSON_AddStringToObject(slot_obj, "slot_config_binary", slot_config_bin);
int key_config_offset = 96 + (slot * 2);
uint8_t key_config_low = config[key_config_offset];
uint8_t key_config_high = config[key_config_offset + 1];
uint16_t key_config = (key_config_high << 8) | key_config_low;
cJSON *key_config_arr = cJSON_CreateArray();
char key_config_low_str[5];
char key_config_high_str[5];
sprintf(key_config_low_str, "0x%02X", key_config_low);
sprintf(key_config_high_str, "0x%02X", key_config_high);
cJSON_AddItemToArray(key_config_arr,
cJSON_CreateString(key_config_low_str));
cJSON_AddItemToArray(key_config_arr,
cJSON_CreateString(key_config_high_str));
cJSON_AddItemToObject(slot_obj, "key_config", key_config_arr);
// Add 16-bit binary representation
char key_config_bin[18];
for (int b = 0; b < 16; b++) {
key_config_bin[15 - b] = ((key_config >> b) & 1) ? '1' : '0';
}
key_config_bin[16] = '\0';
cJSON_AddStringToObject(slot_obj, "key_config_binary", key_config_bin);
cJSON_AddBoolToObject(slot_obj, "is_secret", (slot_config & 0x8000) != 0);
cJSON_AddBoolToObject(slot_obj, "encrypt_read",
(slot_config & 0x4000) != 0);
cJSON_AddBoolToObject(slot_obj, "is_private", (key_config & 0x0001) != 0);
cJSON_AddItemToArray(slots, slot_obj);
}
cJSON_AddItemToObject(root, "slots", slots);
// Additional fields
cJSON_AddNumberToObject(root, "user_extra", config[84]);
cJSON_AddNumberToObject(root, "selector", config[85]);
char *json_str = cJSON_Print(root);
cJSON_Delete(root);
return json_str;
}
/**
* Release ATECC608B resources
*/
void atecc608B_release() { atcab_release(); }
#endif // ATECC608B_H
#endif /* ATECC608B_H */

281
main/main.c Normal file
View File

@@ -0,0 +1,281 @@
/*
* keypitecc door controller
*
* Hardware:
* GPIO 9 Boot button (active-low, press = GND)
* GPIO 15 User LED (active-high, 1 = on)
*
* Button state machine (5-second window):
* IDLE
* └─ press ──► PENDING_OPEN (slow blink, will run SSH open command)
* └─ press ──► PENDING_LOCK (fast blink, will run SSH lock command)
* └─ press ──► IDLE (cancelled)
* Any pending state expires after 5 s → command is executed → IDLE
*
* LED idle behaviour:
* WiFi connected → LED ON
* WiFi disconnected → LED OFF
*/
#include "atecc608a.h"
#include "ssh_client.h"
#include "wifi.h"
#include "sdkconfig.h"
#include <driver/gpio.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/timers.h>
#include <nvs_flash.h>
#include <stdbool.h>
#include <stdint.h>
static const char *TAG = "main";
/* -------------------------------------------------------------------------
* GPIO defines
* ---------------------------------------------------------------------- */
#define GPIO_BTN 9 /* Boot button, active-low */
#define GPIO_LED 15 /* User LED, active-high */
/* -------------------------------------------------------------------------
* Button / LED types
* ---------------------------------------------------------------------- */
typedef enum {
BTN_STATE_IDLE,
BTN_STATE_PENDING_OPEN,
BTN_STATE_PENDING_LOCK,
} btn_state_t;
typedef enum {
LED_MODE_WIFI_STATUS, /* ON = connected, OFF = disconnected */
LED_MODE_SLOW_BLINK, /* 500 ms on / 500 ms off (open pending) */
LED_MODE_FAST_BLINK, /* 100 ms on / 100 ms off (lock pending) */
} led_mode_t;
typedef enum {
EVT_BUTTON_PRESS,
EVT_TIMER_EXPIRED,
} event_type_t;
/* -------------------------------------------------------------------------
* Shared state
* ---------------------------------------------------------------------- */
static volatile led_mode_t s_led_mode = LED_MODE_WIFI_STATUS;
static volatile btn_state_t s_btn_state = BTN_STATE_IDLE;
static QueueHandle_t s_event_queue;
static TimerHandle_t s_action_timer;
/* Debounce: ignore button edges within 200 ms of the previous one */
static volatile TickType_t s_last_btn_tick = 0;
/* -------------------------------------------------------------------------
* SSH task spawned on demand so the button task stays responsive
* ---------------------------------------------------------------------- */
static void ssh_task(void *arg)
{
const char *cmd = (const char *)arg;
ESP_LOGI(TAG, "Executing SSH command: %s", cmd);
bool ok = ssh_execute_command(cmd);
ESP_LOGI(TAG, "SSH command %s: %s", cmd, ok ? "succeeded" : "FAILED");
vTaskDelete(NULL);
}
static void trigger_ssh(const char *cmd)
{
/* Stack size 16 kB SSH needs heap + TLS, keep priority low */
if (xTaskCreate(ssh_task, "ssh", 16384, (void *)cmd, 3, NULL) != pdPASS) {
ESP_LOGE(TAG, "Failed to create SSH task");
}
}
/* -------------------------------------------------------------------------
* FreeRTOS software timer callback runs in timer daemon task context
* ---------------------------------------------------------------------- */
static void action_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
event_type_t evt = EVT_TIMER_EXPIRED;
xQueueSend(s_event_queue, &evt, 0);
}
/* -------------------------------------------------------------------------
* GPIO ISR
* ---------------------------------------------------------------------- */
static void IRAM_ATTR button_isr(void *arg)
{
(void)arg;
TickType_t now = xTaskGetTickCountFromISR();
if ((now - s_last_btn_tick) < pdMS_TO_TICKS(200)) {
return; /* debounce */
}
s_last_btn_tick = now;
event_type_t evt = EVT_BUTTON_PRESS;
xQueueSendFromISR(s_event_queue, &evt, NULL);
}
/* -------------------------------------------------------------------------
* Button task owns the state machine
* ---------------------------------------------------------------------- */
static void button_task(void *arg)
{
(void)arg;
event_type_t evt;
for (;;) {
if (xQueueReceive(s_event_queue, &evt, portMAX_DELAY) != pdTRUE) {
continue;
}
switch (s_btn_state) {
case BTN_STATE_IDLE:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: PENDING_OPEN in 5s (press again for LOCK, third press cancels)");
s_btn_state = BTN_STATE_PENDING_OPEN;
s_led_mode = LED_MODE_SLOW_BLINK;
xTimerStart(s_action_timer, 0);
}
break;
case BTN_STATE_PENDING_OPEN:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: switching to PENDING_LOCK");
s_btn_state = BTN_STATE_PENDING_LOCK;
s_led_mode = LED_MODE_FAST_BLINK;
xTimerReset(s_action_timer, 0); /* restart 5s from now */
} else if (evt == EVT_TIMER_EXPIRED) {
ESP_LOGI(TAG, "Timer expired: executing OPEN command");
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
trigger_ssh(CONFIG_SSH_OPEN_COMMAND);
}
break;
case BTN_STATE_PENDING_LOCK:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: action CANCELLED");
xTimerStop(s_action_timer, 0);
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
} else if (evt == EVT_TIMER_EXPIRED) {
ESP_LOGI(TAG, "Timer expired: executing LOCK command");
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
trigger_ssh(CONFIG_SSH_LOCK_COMMAND);
}
break;
}
}
}
/* -------------------------------------------------------------------------
* LED task drives GPIO 15 according to the current led_mode
* ---------------------------------------------------------------------- */
static void led_task(void *arg)
{
(void)arg;
bool led_state = false;
for (;;) {
led_mode_t mode = s_led_mode; /* atomic read (single-byte enum) */
switch (mode) {
case LED_MODE_WIFI_STATUS:
led_state = wifi_is_connected();
gpio_set_level(GPIO_LED, led_state ? 0 : 1);
vTaskDelay(pdMS_TO_TICKS(250));
break;
case LED_MODE_SLOW_BLINK:
gpio_set_level(GPIO_LED, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(GPIO_LED, 0);
vTaskDelay(pdMS_TO_TICKS(500));
break;
case LED_MODE_FAST_BLINK:
gpio_set_level(GPIO_LED, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_LED, 0);
vTaskDelay(pdMS_TO_TICKS(100));
break;
}
}
}
/* -------------------------------------------------------------------------
* GPIO initialisation
* ---------------------------------------------------------------------- */
static void gpio_init(void)
{
/* LED output */
gpio_config_t led_cfg = {
.pin_bit_mask = (1ULL << GPIO_LED),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&led_cfg);
gpio_set_level(GPIO_LED, 1); /* off (active-high) */
/* Button input with interrupt on falling edge (active-low) */
gpio_config_t btn_cfg = {
.pin_bit_mask = (1ULL << GPIO_BTN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&btn_cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_BTN, button_isr, NULL);
}
/* -------------------------------------------------------------------------
* app_main
* ---------------------------------------------------------------------- */
void app_main(void)
{
/* NVS (required by WiFi) */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* ATECC608B */
if (!atecc608B_init()) {
ESP_LOGW(TAG, "ATECC608B init failed SSH authentication will not work");
} else {
atecc608B_print_config();
ssh_print_public_key(); /* print key for authorized_keys setup */
}
/* WiFi */
wifi_init();
/* GPIO and event infrastructure */
gpio_init();
s_event_queue = xQueueCreate(8, sizeof(event_type_t));
configASSERT(s_event_queue);
/* 5-second one-shot timer (auto-reload disabled) */
s_action_timer = xTimerCreate("action", pdMS_TO_TICKS(5000),
pdFALSE, NULL, action_timer_cb);
configASSERT(s_action_timer);
/* Tasks */
xTaskCreate(button_task, "button", 4096, NULL, 5, NULL);
xTaskCreate(led_task, "led", 2048, NULL, 4, NULL);
ESP_LOGI(TAG, "keypitecc ready press the boot button to operate the door");
}

View File

@@ -1,12 +0,0 @@
/*
* ESP32 Tang Server - Main Entry Point
*/
#include "TangServer.h"
extern "C" void app_main(void) {
setup();
while (true) {
loop();
}
}

327
main/ssh_client.c Normal file
View File

@@ -0,0 +1,327 @@
#include "ssh_client.h"
#include "cryptoauthlib.h"
#include "sdkconfig.h"
#include <arpa/inet.h>
#include <esp_log.h>
#include <libssh2.h>
#include <mbedtls/base64.h>
#include <mbedtls/sha256.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
static const char *TAG = "ssh_client";
/* -------------------------------------------------------------------------
* SSH blob / key helpers
* ---------------------------------------------------------------------- */
/* Write a big-endian uint32 into buf. */
static void write_be32(uint8_t *buf, uint32_t val)
{
buf[0] = (val >> 24) & 0xFF;
buf[1] = (val >> 16) & 0xFF;
buf[2] = (val >> 8) & 0xFF;
buf[3] = val & 0xFF;
}
/**
* Format one ECDSA integer as an SSH mpint:
* 4-byte big-endian length | optional 0x00 pad | value (leading zeros stripped)
*
* Returns number of bytes written to buf.
*/
static uint32_t write_mpint(uint8_t *buf, const uint8_t *val, uint32_t size)
{
uint32_t start = 0;
while (start < (size - 1) && val[start] == 0x00) {
start++;
}
bool pad = (val[start] & 0x80) != 0;
uint32_t len = (size - start) + (pad ? 1 : 0);
uint32_t off = 0;
buf[off++] = (len >> 24) & 0xFF;
buf[off++] = (len >> 16) & 0xFF;
buf[off++] = (len >> 8) & 0xFF;
buf[off++] = len & 0xFF;
if (pad) {
buf[off++] = 0x00;
}
memcpy(&buf[off], &val[start], size - start);
return off + (size - start);
}
/**
* Build a 104-byte SSH public key blob for ecdsa-sha2-nistp256:
* [uint32 19] "ecdsa-sha2-nistp256"
* [uint32 8] "nistp256"
* [uint32 65] 0x04 || X(32) || Y(32)
*
* @param atecc_pubkey 64-byte raw public key (X||Y) from ATECC608B.
* @param out_blob Must point to a buffer of at least 104 bytes.
*/
static void build_pubkey_blob(const uint8_t *atecc_pubkey, uint8_t *out_blob)
{
uint32_t off = 0;
/* Key type */
const char *key_type = "ecdsa-sha2-nistp256";
write_be32(&out_blob[off], 19); off += 4;
memcpy(&out_blob[off], key_type, 19); off += 19;
/* Curve name */
const char *curve = "nistp256";
write_be32(&out_blob[off], 8); off += 4;
memcpy(&out_blob[off], curve, 8); off += 8;
/* Uncompressed EC point: 0x04 || X || Y */
write_be32(&out_blob[off], 65); off += 4;
out_blob[off++] = 0x04;
memcpy(&out_blob[off], atecc_pubkey, 64);
/* off += 64 — total = 104 */
}
/* -------------------------------------------------------------------------
* libssh2 signing callback (ATECC608B signs the hash)
* ---------------------------------------------------------------------- */
static int sign_callback(LIBSSH2_SESSION *session,
unsigned char **sig, size_t *sig_len,
const unsigned char *data, size_t data_len,
void **abstract)
{
(void)session;
(void)abstract;
uint8_t digest[32];
uint8_t raw_sig[64];
/* Hash the challenge data */
mbedtls_sha256(data, data_len, digest, 0);
/* Sign with the ATECC608B hardware key in slot 0 */
if (atcab_sign(0, digest, raw_sig) != ATCA_SUCCESS) {
ESP_LOGE(TAG, "ATECC608B signing failed!");
return -1;
}
/* Encode as [mpint R][mpint S] (no outer string wrapper) */
unsigned char *buf = (unsigned char *)malloc(80);
if (!buf) return -1;
uint32_t off = 0;
off += write_mpint(&buf[off], &raw_sig[0], 32); /* R */
off += write_mpint(&buf[off], &raw_sig[32], 32); /* S */
*sig = buf;
*sig_len = off;
return 0;
}
/* -------------------------------------------------------------------------
* Helpers for non-blocking libssh2
* ---------------------------------------------------------------------- */
static int waitsocket(int sock_fd, LIBSSH2_SESSION *session)
{
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
fd_set fd;
FD_ZERO(&fd);
FD_SET(sock_fd, &fd);
fd_set *rfd = NULL, *wfd = NULL;
int dir = libssh2_session_block_directions(session);
if (dir & LIBSSH2_SESSION_BLOCK_INBOUND) rfd = &fd;
if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) wfd = &fd;
return select(sock_fd + 1, rfd, wfd, NULL, &tv);
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
void ssh_print_public_key(void)
{
uint8_t raw_key[64];
ATCA_STATUS st = atcab_get_pubkey(0, raw_key);
if (st != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st);
return;
}
uint8_t blob[104];
build_pubkey_blob(raw_key, blob);
size_t b64_len = 0;
mbedtls_base64_encode(NULL, 0, &b64_len, blob, sizeof(blob));
unsigned char *b64 = (unsigned char *)malloc(b64_len + 1);
if (!b64) {
ESP_LOGE(TAG, "malloc failed for base64 buffer");
return;
}
mbedtls_base64_encode(b64, b64_len, &b64_len, blob, sizeof(blob));
b64[b64_len] = '\0';
printf("\n=== SSH Public Key (add to authorized_keys) ===\n");
printf("ecdsa-sha2-nistp256 %s keypitecc\n", b64);
printf("================================================\n\n");
free(b64);
}
bool ssh_execute_command(const char *cmd)
{
if (!cmd) return false;
/* --- Read public key blob ------------------------------------------ */
uint8_t raw_key[64];
ATCA_STATUS st = atcab_get_pubkey(0, raw_key);
if (st != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st);
return false;
}
uint8_t pubkey_blob[104];
build_pubkey_blob(raw_key, pubkey_blob);
/* --- TCP connect ------------------------------------------------------- */
int rc;
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
ESP_LOGE(TAG, "socket() failed");
return false;
}
struct sockaddr_in sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(CONFIG_SSH_PORT);
sin.sin_addr.s_addr = inet_addr(CONFIG_SSH_HOSTNAME);
if (sin.sin_addr.s_addr == 0xFFFFFFFF) {
struct hostent *hp = gethostbyname(CONFIG_SSH_HOSTNAME);
if (!hp) {
ESP_LOGE(TAG, "gethostbyname(%s) failed", CONFIG_SSH_HOSTNAME);
close(sock);
return false;
}
sin.sin_addr.s_addr = ((struct ip4_addr *)hp->h_addr)->addr;
}
if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) != 0) {
ESP_LOGE(TAG, "connect to %s:%d failed", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT);
close(sock);
return false;
}
ESP_LOGI(TAG, "TCP connected to %s:%d", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT);
/* --- libssh2 session -------------------------------------------------- */
rc = libssh2_init(0);
if (rc) {
ESP_LOGE(TAG, "libssh2_init failed: %d", rc);
close(sock);
return false;
}
LIBSSH2_SESSION *session = libssh2_session_init();
if (!session) {
ESP_LOGE(TAG, "libssh2_session_init failed");
libssh2_exit();
close(sock);
return false;
}
rc = libssh2_session_handshake(session, sock);
if (rc) {
ESP_LOGE(TAG, "SSH handshake failed: %d", rc);
goto cleanup_session;
}
ESP_LOGI(TAG, "SSH handshake OK");
/* --- Authenticate with ATECC608B hardware key ------------------------- */
void *abstract = NULL;
rc = libssh2_userauth_publickey(session, CONFIG_SSH_USERNAME,
pubkey_blob, sizeof(pubkey_blob),
sign_callback, &abstract);
if (rc != 0) {
ESP_LOGE(TAG, "SSH public-key authentication failed: %d", rc);
goto cleanup_session;
}
ESP_LOGI(TAG, "SSH authenticated as '%s'", CONFIG_SSH_USERNAME);
/* --- Open channel and execute command --------------------------------- */
LIBSSH2_CHANNEL *channel;
do {
channel = libssh2_channel_open_session(session);
if (!channel && libssh2_session_last_errno(session) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(sock, session);
}
} while (!channel);
if (!channel) {
ESP_LOGE(TAG, "Failed to open SSH channel");
goto cleanup_session;
}
ESP_LOGI(TAG, "Executing: %s", cmd);
while ((rc = libssh2_channel_exec(channel, cmd)) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(sock, session);
}
if (rc != 0) {
ESP_LOGE(TAG, "channel_exec failed: %d", rc);
libssh2_channel_free(channel);
goto cleanup_session;
}
/* --- Read output ------------------------------------------------------- */
char buf[256];
int bytes;
ESP_LOGI(TAG, "--- command output ---");
for (;;) {
do {
bytes = libssh2_channel_read(channel, buf, sizeof(buf) - 1);
} while (bytes == LIBSSH2_ERROR_EAGAIN && (waitsocket(sock, session), 1));
if (bytes <= 0) break;
buf[bytes] = '\0';
printf("%s", buf);
}
ESP_LOGI(TAG, "--- end output ---");
/* --- Shutdown --------------------------------------------------------- */
while ((rc = libssh2_channel_send_eof(channel)) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(sock, session);
}
while (libssh2_channel_wait_eof(channel) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(sock, session);
}
while (libssh2_channel_wait_closed(channel) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(sock, session);
}
int exit_status = libssh2_channel_get_exit_status(channel);
ESP_LOGI(TAG, "Command exited with status: %d", exit_status);
libssh2_channel_free(channel);
libssh2_session_disconnect(session, "Bye");
libssh2_session_free(session);
libssh2_exit();
close(sock);
return (exit_status == 0);
cleanup_session:
libssh2_session_disconnect(session, "Error");
libssh2_session_free(session);
libssh2_exit();
close(sock);
return false;
}

25
main/ssh_client.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef SSH_CLIENT_H
#define SSH_CLIENT_H
#include <stdbool.h>
/**
* Read the ATECC608B public key, format it as an SSH authorized_keys entry,
* and print it to the console. Call this once at boot so the key can be
* added to the server's authorized_keys file.
*
* Output format:
* ecdsa-sha2-nistp256 <base64-blob> keypitecc
*/
void ssh_print_public_key(void);
/**
* Open a TCP connection to the configured SSH server, authenticate using the
* ATECC608B hardware key, execute @p cmd, log the output, and disconnect.
*
* @param cmd Shell command string to run on the remote host.
* @return true on success, false on any error.
*/
bool ssh_execute_command(const char *cmd);
#endif /* SSH_CLIENT_H */

81
main/wifi.c Normal file
View File

@@ -0,0 +1,81 @@
#include "wifi.h"
#include "sdkconfig.h"
#include <esp_event.h>
#include <esp_log.h>
#include <esp_netif.h>
#include <esp_wifi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <string.h>
static const char *TAG = "wifi";
static EventGroupHandle_t s_wifi_event_group;
static const int WIFI_CONNECTED_BIT = BIT0;
static volatile bool s_connected = false;
/* -------------------------------------------------------------------------
* Event handler
* ---------------------------------------------------------------------- */
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi connecting...");
} else if (event_base == WIFI_EVENT &&
event_id == WIFI_EVENT_STA_DISCONNECTED) {
s_connected = false;
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi disconnected, reconnecting...");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "WiFi connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
s_connected = true;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
void wifi_init(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
configASSERT(sta_netif);
ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, "keypitecc"));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL));
wifi_config_t wifi_config = {};
strncpy((char *)wifi_config.sta.ssid, CONFIG_WIFI_SSID,
sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, CONFIG_WIFI_PASSWORD,
sizeof(wifi_config.sta.password) - 1);
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Connecting to SSID: %s", CONFIG_WIFI_SSID);
}
bool wifi_is_connected(void)
{
return s_connected;
}

19
main/wifi.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef WIFI_H
#define WIFI_H
#include <stdbool.h>
/**
* Initialise WiFi in STA mode, connect to the SSID defined in Kconfig,
* and register event handlers. Blocks until a first connection attempt
* is made, but does NOT block forever reconnection is handled in the
* background.
*/
void wifi_init(void);
/**
* Returns true when the station has a valid IP address (i.e. WiFi is up).
*/
bool wifi_is_connected(void);
#endif /* WIFI_H */

View File

@@ -1,411 +0,0 @@
#ifndef ZK_AUTH_H
#define ZK_AUTH_H
#include "cryptoauthlib.h"
#include "host/atca_host.h"
#include "libssh2_config.h"
#include "mbedtls/sha256.h"
#include "netdb.h"
#include <cJSON.h>
#include <esp_mac.h>
#include <esp_system.h>
#include <libssh2.h>
#include <mbedtls/aes.h>
#include <mbedtls/base64.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdh.h>
#include <mbedtls/ecp.h>
#include <mbedtls/entropy.h>
#include <mbedtls/md.h>
#include <mbedtls/pkcs5.h>
#include <mbedtls/sha256.h>
#include <stdio.h>
#include <string.h>
// Added network headers for ESP-IDF (lwIP) sockets
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#define BUFSIZE 3200
class ZKAuth {
private:
mbedtls_ecp_group grp;
mbedtls_mpi device_private_d;
mbedtls_ecp_point device_public_Q;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
uint8_t device_public_key[65]; // Uncompressed: 0x04 + X(32) + Y(32)
uint8_t stored_password_hash[32]; // PBKDF2 hash of the correct password
bool initialized;
// Convert binary to hex string
void bin_to_hex(const uint8_t *bin, size_t bin_len, char *hex) {
for (size_t i = 0; i < bin_len; i++) {
sprintf(hex + (i * 2), "%02x", bin[i]);
}
hex[bin_len * 2] = '\0';
}
// Convert hex string to binary
bool hex_to_bin(const char *hex, uint8_t *bin, size_t bin_len) {
if (strlen(hex) != bin_len * 2)
return false;
for (size_t i = 0; i < bin_len; i++) {
if (sscanf(hex + (i * 2), "%2hhx", &bin[i]) != 1) {
return false;
}
}
return true;
}
public:
ZKAuth() : initialized(false) {
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&device_private_d);
mbedtls_ecp_point_init(&device_public_Q);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
}
~ZKAuth() {
mbedtls_ecp_group_free(&grp);
mbedtls_mpi_free(&device_private_d);
mbedtls_ecp_point_free(&device_public_Q);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
}
// Initialize the ZK authentication system
bool init() {
if (initialized) {
return true;
} else {
initialized = true;
printf("\n=== ZK Authentication Initialized ===\n");
}
return true;
}
// Marked as static so it can be passed as a C callback to libssh2
// Helper to safely format SSH integers (mpint) exactly how OpenSSH demands
static uint32_t write_mpint(uint8_t *buf, const uint8_t *val, uint32_t size) {
uint32_t start = 0;
// Strip unnecessary leading zeros
while (start < (size - 1) && val[start] == 0x00) {
start++;
}
// If the most significant bit is 1, we must prepend a 0x00 padding byte
bool pad = (val[start] & 0x80) != 0;
uint32_t len = (size - start) + (pad ? 1 : 0);
uint32_t offset = 0;
buf[offset++] = (len >> 24) & 0xFF;
buf[offset++] = (len >> 16) & 0xFF;
buf[offset++] = (len >> 8) & 0xFF;
buf[offset++] = len & 0xFF;
if (pad) {
buf[offset++] = 0x00;
}
memcpy(&buf[offset], &val[start], size - start);
return offset + (size - start);
}
// The main signing callback
static int sign_callback(LIBSSH2_SESSION *session, unsigned char **sig,
size_t *sig_len, const unsigned char *data,
size_t data_len, void **abstract) {
uint8_t digest[32];
uint8_t raw_sig[64];
// 1. Hash the fully assembled challenge buffer provided by libssh2
mbedtls_sha256(data, data_len, digest, 0);
// 2. Request signature from the ATECC608B
if (atcab_sign(0x0, digest, raw_sig) != ATCA_SUCCESS) {
printf("CRITICAL: ATECC608B Signing Failed!\n");
return -1;
}
// 3. Allocate memory JUST for the two mathematical integers (max ~74 bytes)
unsigned char *buf = (unsigned char *)malloc(80);
if (!buf)
return -1;
// 4. Format strictly as [mpint R] [mpint S]. NO outer strings!
uint32_t offset = 0;
offset += write_mpint(&buf[offset], &raw_sig[0], 32); // Format R
offset += write_mpint(&buf[offset], &raw_sig[32], 32); // Format S
// Hand ownership to libssh2
*sig = buf;
*sig_len = offset;
return 0; // Success!
}
// Helper function to write a 32-bit integer in big-endian format
void write_uint32_be(uint8_t *buf, uint32_t val) {
buf[0] = (val >> 24) & 0xFF;
buf[1] = (val >> 16) & 0xFF;
buf[2] = (val >> 8) & 0xFF;
buf[3] = val & 0xFF;
}
// Function to generate the authorized_keys string
void generate_ssh_authorized_key(const uint8_t *atecc_pubkey,
uint8_t *out_blob) {
uint8_t ssh_blob[104];
uint32_t offset = 0;
// 1. Key Type
const char *key_type = "ecdsa-sha2-nistp256";
write_uint32_be(&ssh_blob[offset], 19);
offset += 4;
memcpy(&ssh_blob[offset], key_type, 19);
offset += 19;
// 2. Curve Name
const char *curve_name = "nistp256";
write_uint32_be(&ssh_blob[offset], 8);
offset += 4;
memcpy(&ssh_blob[offset], curve_name, 8);
offset += 8;
// 3. Public Key (Uncompressed format: 0x04 + 64 bytes X/Y)
write_uint32_be(&ssh_blob[offset], 65);
offset += 4;
ssh_blob[offset++] = 0x04;
memcpy(&ssh_blob[offset], atecc_pubkey, 64);
offset += 64;
memcpy(out_blob, ssh_blob, 104);
}
static void my_trace_handler(LIBSSH2_SESSION *session, void *context,
const char *data, size_t length) {
// Print the trace data to the console
printf("LIBSSH2_TRACE: %.*s", (int)length, data);
}
static int waitsocket(int socket_fd, LIBSSH2_SESSION *session) {
struct timeval timeout;
int rc;
fd_set fd;
fd_set *writefd = NULL;
fd_set *readfd = NULL;
int dir;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
FD_ZERO(&fd);
FD_SET(socket_fd, &fd);
/* now make sure we wait in the correct direction */
dir = libssh2_session_block_directions(session);
if (dir & LIBSSH2_SESSION_BLOCK_INBOUND)
readfd = &fd;
if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND)
writefd = &fd;
rc = select(socket_fd + 1, readfd, writefd, NULL, &timeout);
return rc;
}
// Get device identity (for /api/identity endpoint)
char *get_identity_json() {
uint8_t atecc_pubkey[64]; // 65 bytes * 2 + null
// Get public key from ATECC608B and convert to hex
ATCA_STATUS status = atcab_get_pubkey(0, atecc_pubkey);
if (status != ATCA_SUCCESS) {
printf("Failed to read public key from ATECC608B: 0x%02X\n", status);
}
uint8_t pubkey_blob[104];
generate_ssh_authorized_key(atecc_pubkey, pubkey_blob);
// 4. Base64 Encode the blob
size_t b64_len = 0;
mbedtls_base64_encode(NULL, 0, &b64_len, pubkey_blob, 104);
unsigned char b64_out[b64_len];
mbedtls_base64_encode(b64_out, b64_len, &b64_len, pubkey_blob, 104);
printf("ecdsa-sha2-nistp256 %s esp32-atecc608b\n", b64_out);
// Concat type string with pubkey and identifier for authorized_keys format
char authorized_keys_line[150]; // 19 + 1 + b64_len + 1 + 13 + 1 = 150
snprintf(authorized_keys_line, sizeof(authorized_keys_line),
"ecdsa-sha2-nistp256 %s esp32-atecc608b", b64_out);
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "sshPublicKey", authorized_keys_line);
char *json_str = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
const char *username = "jonathan";
int sock;
struct sockaddr_in sin;
const char *hostname = "192.168.178.86";
unsigned short port = 22;
void *my_abstract = NULL;
LIBSSH2_SESSION *session;
ESP_LOGI(TAG, "libssh2_version is %s", LIBSSH2_VERSION);
int rc = libssh2_init(0);
if (rc) {
ESP_LOGE(TAG, "libssh2 initialization failed (%d)", rc);
while (1) {
vTaskDelay(1);
}
}
ESP_LOGD(TAG, "hostname=%s", hostname);
ESP_LOGD(TAG, "port=%d", port);
sin.sin_family = AF_INET;
// sin.sin_port = htons(22);
sin.sin_port = htons(port);
sin.sin_addr.s_addr = inet_addr(hostname);
ESP_LOGD(TAG, "sin.sin_addr.s_addr=%" PRIx32, sin.sin_addr.s_addr);
if (sin.sin_addr.s_addr == 0xffffffff) {
struct hostent *hp;
hp = gethostbyname(hostname);
if (hp == NULL) {
ESP_LOGE(TAG, "gethostbyname fail");
ESP_LOGE(TAG, "hostname=%s", hostname);
ESP_LOGE(TAG, "port=%d", port);
while (1) {
vTaskDelay(1);
}
}
struct ip4_addr *ip4_addr;
ip4_addr = (struct ip4_addr *)hp->h_addr;
sin.sin_addr.s_addr = ip4_addr->addr;
ESP_LOGD(TAG, "sin.sin_addr.s_addr=%" PRIx32, sin.sin_addr.s_addr);
}
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
ESP_LOGE(TAG, "failed to create socket!");
while (1) {
vTaskDelay(1);
}
}
if (connect(sock, (struct sockaddr *)(&sin), sizeof(struct sockaddr_in)) !=
0) {
ESP_LOGE(TAG, "failed to connect!");
ESP_LOGE(TAG, "hostname=%s", hostname);
ESP_LOGE(TAG, "port=%d", port);
while (1) {
vTaskDelay(1);
}
}
printf("Connected. Starting SSH handshake...\n");
session = libssh2_session_init();
if (!session) {
ESP_LOGE(TAG, "failed to session init");
while (1) {
vTaskDelay(1);
}
}
// Enables extensive logging from libssh2
// Needs debug logging enabled in menuconfig > components > libssh2
// libssh2_trace(session, ~0);
// libssh2_trace_sethandler(session, NULL, my_trace_handler);
printf("Session initialized. Setting timeout...\n");
// libssh2_session_set_timeout(session, 10000);
printf("Performing handshake...\n");
rc = libssh2_session_handshake(session, sock);
if (rc) {
ESP_LOGE(TAG, "Failure establishing SSH session: %d", rc);
while (1) {
vTaskDelay(1);
}
}
printf("Handshake completed. Authenticating with public key...\n");
// PUBLIC KEY AUTH: using custom callback
rc = libssh2_userauth_publickey(session, username, pubkey_blob,
sizeof(pubkey_blob), sign_callback,
&my_abstract);
if (rc == 0) {
printf("SSH authentication successful!\n");
// 1. Open a channel within the authenticated session
LIBSSH2_CHANNEL *channel = libssh2_channel_open_session(session);
if (!channel) {
printf("Failed to open a session channel!\n");
} else {
// 2. Execute the command
const char *cmd = "ls -la";
printf("Executing command: %s\n", cmd);
rc = libssh2_channel_exec(channel, cmd);
if (rc != 0) {
printf("Failed to execute command. Error: %d\n", rc);
} else {
printf("--- Command Output ---\n");
char buffer[256];
int bytes_read;
// 3. Read the output in a loop until the channel closes (EOF)
// libssh2_channel_read returns the amount of bytes read, 0 on EOF, or
// <0 on error
while ((bytes_read = libssh2_channel_read(channel, buffer,
sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] =
'\0'; // Null-terminate the chunk so printf handles it safely
printf("%s", buffer);
}
if (bytes_read < 0) {
printf("\n[Read failed with error code: %d]\n", bytes_read);
}
printf("\n----------------------\n");
}
// 4. Gracefully close the channel and grab the exit code (e.g., 0 for
// success)
libssh2_channel_close(channel);
int exit_status = libssh2_channel_get_exit_status(channel);
printf("Command exited with status: %d\n", exit_status);
// 5. Free the channel memory
libssh2_channel_free(channel);
}
} else {
printf("Authentication failed\n");
}
// Cleanup
libssh2_session_disconnect(session, "Bye");
libssh2_session_free(session);
close(sock);
libssh2_exit();
return json_str;
}
};
#endif // ZK_AUTH_H

View File

@@ -1,50 +0,0 @@
#ifndef ZK_HANDLERS_H
#define ZK_HANDLERS_H
#include "zk_auth.h"
#include <esp_http_server.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdsa.h>
#include <mbedtls/ecp.h>
#include <mbedtls/entropy.h>
#include <mbedtls/gcm.h>
#include <mbedtls/md.h>
#include <mbedtls/pkcs5.h>
#include <mbedtls/sha256.h>
#include <mbedtls/sha512.h>
#include <string.h>
// Global ZK Auth instance (to be initialized in main)
extern ZKAuth zk_auth;
extern httpd_handle_t server_http;
// API endpoint: Get device identity
static esp_err_t handle_zk_identity(httpd_req_t *req) {
char *json_response = zk_auth.get_identity_json();
if (json_response == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to get identity");
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_sendstr(req, json_response);
free(json_response);
return ESP_OK;
}
// API endpoint: Process unlock request
// Handle CORS preflight
// Register all ZK auth routes to the HTTP server
void register_zk_handlers(httpd_handle_t server) {
httpd_uri_t identity_uri = {.uri = "/api/identity",
.method = HTTP_GET,
.handler = handle_zk_identity,
.user_ctx = NULL};
httpd_register_uri_handler(server, &identity_uri);
}
#endif // ZK_HANDLERS_H

View File

@@ -1,9 +1,13 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.2 Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c6"
CONFIG_WIFI_SSID="DoNotSetTheRealValueHere"
CONFIG_WIFI_PASSWORD="PutTheRealPassInTheSdkconfigFile"
CONFIG_IDF_TARGET="esp32c6"
CONFIG_SSH_HOSTNAME="192.168.178.1"
CONFIG_SSH_USERNAME="user"
CONFIG_SSH_OPEN_COMMAND="open"
CONFIG_SSH_LOCK_COMMAND="lock"
CONFIG_ATECC608A_TCUSTOM=y
CONFIG_ATCA_I2C_SDA_PIN=22
CONFIG_ATCA_I2C_SCL_PIN=23