commit 8e95149d8e7a1dc6133cf9d4082ab2a871562792 Author: Jonathan Berrisch Date: Fri Feb 27 22:17:33 2026 +0100 Init diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..67bfde5 --- /dev/null +++ b/.envrc @@ -0,0 +1,31 @@ +# disable nix builders because they are slow for copy operations +export NIX_BUILDERS="" + +# Automatically load the Nix development environment when entering this directory +use flake + +# Export ESP-IDF related environment variables +export IDF_TOOLS_PATH="$HOME/.espressif" +export CCACHE_DIR="$HOME/.ccache" + +# Prevent Python bytecode generation +export PYTHONDONTWRITEBYTECODE=1 + +# Enable colored output for various tools +export FORCE_COLOR=1 + +# Set common ESP32 development variables +export ESP_TARGET=esp32 +export ESP_BOARD=esp32dev + +# Set locale to avoid issues +export LANG=C.UTF-8 +export LC_ALL=C.UTF-8 + +# Create necessary directories +mkdir -p "$IDF_TOOLS_PATH" +mkdir -p "$CCACHE_DIR" + +# Show a message when the environment is loaded +echo "🚀 ESP32 Tang Server environment loaded!" +echo "ESP-IDF ready - run 'make help' for available commands" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28953ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Linker files +*.ilk + +# Debugger Files +*.pdb + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# debug information files +*.dwo + +# direnv +.direnv + +# gitignore template for esp-idf, the official development framework for ESP32 +# https://github.com/espressif/esp-idf +build/ +sdkconfig +sdkconfig.old + +managed_components/ +.cache/ + +# Cryptographic files +*.jwk +*.jwe + +# Other Binary files +*.pdf \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..0af50ed --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "NixOS", + "compileCommands": [ + "${workspaceFolder}/build/compile_commands.json" + ], + "includePath": [ + "${workspaceFolder}/**", + "${workspaceFolder}/managed_components/**", + "${workspaceFolder}/managed_components/esp-cryptoauthlib/**" + ], + "compilerPath": "/nix/store/x45d95acx6rybkb5cidrxxffval9k2pg-gcc-wrapper-14.3.0/bin/gcc" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8dfb64c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cmake.automaticReconfigure": true, + "python-envs.defaultEnvManager": "ms-python.python:system", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..dcf845d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +# Top-level CMakeLists.txt for ESP32 Tang Server +cmake_minimum_required(VERSION 3.16) + +# Set the project name +set(PROJECT_NAME "esp32-tang") + +# Include ESP-IDF build system +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# Define the project +project(${PROJECT_NAME}) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd39382 --- /dev/null +++ b/Makefile @@ -0,0 +1,183 @@ +# ESP32 Tang Server Makefile +# Convenient shortcuts for ESP-IDF development tasks + +.DEFAULT_GOAL := help + +# Configuration variables +PORT ?= /dev/ttyUSB0 +BAUD ?= 115200 +TARGET ?= esp32 + +# Help target +.PHONY: help +help: + @echo "ESP32 Tang Server Development Commands" + @echo "=====================================" + @echo "" + @echo "Setup:" + @echo " setup-target Set ESP32 as build target" + @echo " menuconfig Open configuration menu" + @echo "" + @echo "Build:" + @echo " build Build the project" + @echo " clean Clean build files" + @echo " fullclean Full clean (including config)" + @echo "" + @echo "Flash & Monitor:" + @echo " flash Flash to device" + @echo " monitor Open serial monitor" + @echo " flash-monitor Flash and immediately monitor" + @echo "" + @echo "Development:" + @echo " size Show binary size analysis" + @echo " erase Erase flash completely" + @echo " bootloader Flash bootloader only" + @echo "" + @echo "Board Management:" + @echo " list-boards List connected boards" + @echo " detect-port Detect serial ports" + @echo " board-info Show board information" + @echo "" + @echo "Environment variables:" + @echo " PORT=/dev/ttyUSB0 (default serial port)" + @echo " BAUD=115200 (default baud rate)" + @echo " TARGET=esp32 (default target)" + +# Setup commands +.PHONY: setup-target +setup-target: + idf.py set-target $(TARGET) + +.PHONY: menuconfig +menuconfig: + idf.py menuconfig + +# Build commands +.PHONY: build +build: + idf.py build + +.PHONY: clean +clean: + idf.py clean + +.PHONY: fullclean +fullclean: + idf.py fullclean + +# Flash and monitor commands +.PHONY: flash +flash: + idf.py -p $(PORT) -b $(BAUD) flash + +.PHONY: monitor +monitor: + idf.py -p $(PORT) -b $(BAUD) monitor + +.PHONY: flash-monitor +flash-monitor: + idf.py -p $(PORT) -b $(BAUD) flash monitor + +# Development utilities +.PHONY: size +size: + idf.py size + +.PHONY: size-components +size-components: + idf.py size-components + +.PHONY: size-files +size-files: + idf.py size-files + +.PHONY: erase +erase: + idf.py -p $(PORT) erase-flash + +.PHONY: bootloader +bootloader: + idf.py -p $(PORT) bootloader-flash + +# Show partition table +.PHONY: partition-table +partition-table: + idf.py partition-table + +# Generate compilation database for IDE support +.PHONY: compile-commands +compile-commands: + idf.py build --cmake-args="-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" + +# Development shortcuts +.PHONY: dev +dev: build flash-monitor + +.PHONY: quick +quick: build flash + +# Board management +.PHONY: list-boards +list-boards: + idf.py -p $(PORT) board_info || echo "Connect board to $(PORT)" + +.PHONY: detect-port +detect-port: + @echo "Scanning for ESP32 devices..." + @if ls /dev/ttyUSB* >/dev/null 2>&1; then \ + echo "Found USB serial devices:"; \ + ls -la /dev/ttyUSB* | head -5; \ + elif ls /dev/ttyACM* >/dev/null 2>&1; then \ + echo "Found ACM serial devices:"; \ + ls -la /dev/ttyACM* | head -5; \ + else \ + echo "No serial devices found. Make sure ESP32 is connected."; \ + fi + +.PHONY: board-info +board-info: + @echo "Board Information:" + @echo "==================" + @echo "Target: $(TARGET)" + @echo "Port: $(PORT)" + @echo "Baud: $(BAUD)" + @echo "" + @echo "ESP-IDF Version:" + @idf.py --version + @echo "" + @echo "Project status:" + @idf.py show-port-info -p $(PORT) 2>/dev/null || echo "No device connected to $(PORT)" + +# Show ESP-IDF version and tools +.PHONY: version +version: + @echo "ESP-IDF Version Information:" + @idf.py --version + @echo "" + @echo "Toolchain versions:" + @xtensa-esp32-elf-gcc --version | head -1 || echo "Toolchain not found" + @python3 --version + @cmake --version | head -1 + +# Validate project structure +.PHONY: validate +validate: + @echo "Validating ESP-IDF project structure..." + @if [ ! -f "CMakeLists.txt" ]; then \ + echo "Error: CMakeLists.txt not found"; \ + exit 1; \ + fi + @if [ ! -d "main" ]; then \ + echo "Error: main directory not found"; \ + exit 1; \ + fi + @echo "Project validation passed!" + +# Full development cycle +.PHONY: full-setup +full-setup: setup-target menuconfig build + @echo "" + @echo "Full setup complete! You can now:" + @echo " make flash # Flash to device" + @echo " make monitor # View serial output" + @echo " make dev # Flash and monitor" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d4aea3 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# ESP32 Tang Server + +An experimental implementation of a **Tang server** running directly on an **ESP32** device. +The server is written in **C++**, using **mbedTLS** and the **ESP-IDF** framework. + +## Overview + +The goal of this project is to implement the core Tang functionality — **advertisement** and **activation** — directly on the ESP32, demonstrating that a small embedded system can operate as a self-contained cryptographic service. + +In future iterations, this implementation will be **integrated into ESPHome**, enabling seamless use with **Home Assistant**. This will allow ESP-based devices to provide secure key exchange mechanisms within **IoT** or **home automation** environments. +Because HTTPS/SSL will be handled by ESPHome, it is **not** a primary focus of this standalone implementation. + +A distributed deployment with multiple ESP32 Tang servers could further enhance security by requiring responses from several devices for key recovery, reducing single points of failure. + +## Usage + +### Activate the server + +```bash +curl http:///pub > server_pub.jwk +echo -n "change-me" | jose jwe enc -I- -k server_pub.jwk -o request.jwe -i '{"protected":{"enc":"A128GCM"}}' +curl -X POST -H "Content-Type: application/json" -d @request.jwe http:///activate +``` + +### Test the server + +```bash +curl http:///adv +``` + +## Useful Links + +- [Tang Server (reference implementation)](https://github.com/latchset/tang) diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..57a04a6 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,21 @@ +dependencies: + esp-cryptoauthlib: + component_hash: 8d24c37df1e906f9bbee9a869ca86f263ab135179d1c2ba6062448269437b192 + dependencies: + - name: idf + version: '>=4.3' + source: + git: https://github.com/espressif/esp-cryptoauthlib.git + path: . + type: git + version: d9792119ebaec0c54839e6605acd3f11dd937205 + idf: + source: + type: idf + version: 5.5.2 +direct_dependencies: +- esp-cryptoauthlib +- idf +manifest_hash: c756397bc94626ff7204b9330f85430937d2c27c736bf3075be0f87eb8eb0efc +target: esp32c6 +version: 2.0.0 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..468b1a1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,117 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771419570, + "narHash": "sha256-bxAlQgre3pcQcaRUm/8A0v/X8d2nhfraWSFqVmMcBcU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6d41bc27aaf7b6a3ba6b169db3bd5d6159cfaa47", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-esp-dev": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1767865407, + "narHash": "sha256-QWF1rZYd+HvNzLIeRS+OEBX7HF0EhWCGeLbMkgtbsIo=", + "owner": "mirrexagon", + "repo": "nixpkgs-esp-dev", + "rev": "5287d6e1ca9e15ebd5113c41b9590c468e1e001b", + "type": "github" + }, + "original": { + "owner": "mirrexagon", + "repo": "nixpkgs-esp-dev", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs-esp-dev", + "nixpkgs" + ], + "nixpkgs-esp-dev": "nixpkgs-esp-dev" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5589d6c --- /dev/null +++ b/flake.nix @@ -0,0 +1,189 @@ +{ + description = "ESP32 Tang Server Development Environment"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs-esp-dev = { + url = "github:mirrexagon/nixpkgs-esp-dev"; + }; + nixpkgs.follows = "nixpkgs-esp-dev/nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + nixpkgs-esp-dev, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + config = { + permittedInsecurePackages = [ + "python3.13-ecdsa-0.19.1" + ]; + }; + overlays = [ nixpkgs-esp-dev.overlays.default ]; + }; + in + { + devShells.default = pkgs.mkShell { + name = "esp32-tang-dev"; + + buildInputs = + with pkgs; + [ + python3Packages.pandas + + # ESP-IDF with full toolchain + esp-idf-full + jose + clevis + + # Development tools + gnumake + cmake + ninja + ccache + + # Serial communication tools + picocom + screen + minicom + + # Development utilities + curl + wget + unzip + file + which + jq + + # Text processing tools + gawk + gnused + gnugrep + + # Optional development tools + clang-tools + bear + ] + ++ lib.optionals stdenv.isLinux [ + # Linux-specific packages for USB device access + udev + libusb1 + ]; + + shellHook = '' + echo "🚀 ESP32 Tang Server Development Environment" + echo "=============================================" + echo + echo "ESP-IDF: $(idf.py --version 2>/dev/null || echo 'Ready')" + echo "Python: $(python3 --version)" + echo "CMake: $(cmake --version | head -1)" + echo + echo "Available commands:" + echo " 📦 idf.py build - Build the project" + echo " 📡 idf.py flash - Flash to device" + echo " 💻 idf.py monitor - Serial monitor" + echo " 🔧 idf.py flash monitor - Flash and monitor" + echo " ⚙️ idf.py menuconfig - Configuration menu" + echo " 🎯 idf.py set-target - Set target (esp32)" + echo + echo "Or use the Makefile shortcuts:" + echo " make setup-target - Set ESP32 target" + echo " make build - Build project" + echo " make flash-monitor - Flash and monitor" + echo " make menuconfig - Configuration" + echo + echo "Quick start:" + echo " 1. Set target: make setup-target" + echo " 2. Configure: make menuconfig" + echo " 3. Build: make build" + echo " 4. Flash & Monitor: make flash-monitor PORT=/dev/ttyUSB0" + echo + + # Set up development environment + export IDF_TOOLS_PATH="$HOME/.espressif" + export CCACHE_DIR="$HOME/.ccache" + + # Create necessary directories + mkdir -p "$IDF_TOOLS_PATH" + mkdir -p "$CCACHE_DIR" + + # Check for serial devices + if ls /dev/ttyUSB* >/dev/null 2>&1; then + echo "📡 Found USB serial devices:" + ls -la /dev/ttyUSB* 2>/dev/null + elif ls /dev/ttyACM* >/dev/null 2>&1; then + echo "📡 Found ACM serial devices:" + ls -la /dev/ttyACM* 2>/dev/null + else + echo "📡 No serial devices found. Connect your ESP32 board." + fi + + # Check serial permissions + if ! groups | grep -q dialout 2>/dev/null && ! groups | grep -q uucp 2>/dev/null; then + echo + echo "⚠️ Note: You may need to add your user to the 'dialout' group" + echo " to access serial devices:" + echo " sudo usermod -a -G dialout $USER" + echo " Then log out and log back in." + fi + + echo + echo "Ready for ESP32 development with Arduino support! 🎯" + echo + ''; + + # allow /dev/ access + extraDevPaths = [ + "/dev/ttyUSB*" + "/dev/ttyACM*" + ]; + + # Environment variables + IDF_TOOLS_PATH = "$HOME/.espressif"; + CCACHE_DIR = "$HOME/.ccache"; + + # Prevent Python from creating __pycache__ directories + PYTHONDONTWRITEBYTECODE = "1"; + + # Enable colored output + FORCE_COLOR = "1"; + + # Set locale to avoid issues + LANG = "C.UTF-8"; + LC_ALL = "C.UTF-8"; + }; + + devShells.minimal = pkgs.mkShell { + name = "esp32-tang-minimal"; + + buildInputs = with pkgs; [ + esp-idf-full + picocom + ]; + + shellHook = '' + echo "⚡ ESP32 Tang Server (Minimal Environment)" + echo "========================================" + echo "Ready for ESP32 development!" + echo + + export IDF_TOOLS_PATH="$HOME/.espressif" + export CCACHE_DIR="$HOME/.ccache" + mkdir -p "$CCACHE_DIR" + ''; + + extraDevPaths = [ + "/dev/ttyUSB*" + "/dev/ttyACM*" + ]; + }; + } + ); +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..f2cbcc8 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "main.cpp" + INCLUDE_DIRS "." + REQUIRES mbedtls esp-cryptoauthlib esp_http_server esp_wifi nvs_flash json esp_timer efuse) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..ffd57da --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,15 @@ +menu "Wi-Fi Credentials" + +config WIFI_SSID + string "Wi-Fi SSID" + default "" + help + SSID of the Wi-Fi network to connect to. + +config WIFI_PASSWORD + string "Wi-Fi Password" + default "" + help + Password for the Wi-Fi network. + +endmenu diff --git a/main/TangServer.h b/main/TangServer.h new file mode 100644 index 0000000..5a31378 --- /dev/null +++ b/main/TangServer.h @@ -0,0 +1,241 @@ +#ifndef TANG_SERVER_H +#define TANG_SERVER_H + +#include "sdkconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "TangServer"; + +// Include core components +#include "atecc608a.h" +#include "crypto.h" +#include "encoding.h" +#include "provision.h" +#include "provision_handlers.h" +#include "tang_handlers.h" +#include "tang_storage.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; +TangKeyStore keystore; +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() { + + if (!P256::generate_keypair(keystore.exc_pub, keystore.exc_priv)) { + ESP_LOGE(TAG, "ERROR: Failed to generate exchange key"); + return false; + } + + // Save Tang keys directly (no encryption in prototype) + if (!keystore.save_tang_keys()) { + ESP_LOGE(TAG, "ERROR: Failed to save Tang keys"); + return false; + } + + ESP_LOGI(TAG, "Configuration saved to NVS"); + + 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_provision_handlers(server); + + register_zk_handlers(server); + + // Register Tang protocol handlers + httpd_uri_t adv_uri = {.uri = "/adv", + .method = HTTP_GET, + .handler = handle_adv, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &adv_uri); + + httpd_uri_t adv_uri_slash = {.uri = "/adv/", + .method = HTTP_GET, + .handler = handle_adv, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &adv_uri_slash); + + httpd_uri_t rec_uri = {.uri = "/rec", + .method = HTTP_POST, + .handler = handle_rec, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &rec_uri); + + httpd_uri_t config_uri = {.uri = "/config", + .method = HTTP_GET, + .handler = handle_config, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &config_uri); + + httpd_uri_t reboot_uri = {.uri = "/reboot", + .method = HTTP_GET, + .handler = handle_reboot, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &reboot_uri); + + // Register custom error handler for 404 + httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, handle_not_found); + + ESP_LOGI(TAG, "HTTP server listening on port 80"); + } else { + ESP_LOGE(TAG, "Failed to start HTTP server"); + } + + return server; +} + +// --- Main Setup --- +void setup() { + ESP_LOGI(TAG, "\n\nESP32 Tang Server Starting..."); + + // 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"); + + // Initialize ATECC608A + if (atecc608B_init()) { + atecc608B_print_config(); + } else { + ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed"); + } + + // Load or initialize configuration + if (keystore.is_configured()) { + ESP_LOGI(TAG, "Found existing configuration"); + // Auto-load Tang keys on startup (no activation needed in prototype) + if (keystore.load_tang_keys()) { + ESP_LOGI(TAG, "Loaded Tang keys - server ready"); + } else { + ESP_LOGW(TAG, "Failed to load Tang keys"); + } + } else { + perform_initial_setup(); + } + + // 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:///"); + ESP_LOGI(TAG, " - Tang Server: http:///adv"); + } +} + +// --- Main Loop --- +void loop() { + // Just delay - HTTP server handles requests in its own task + vTaskDelay(pdMS_TO_TICKS(1000)); +} + +#endif // TANG_SERVER_H diff --git a/main/atecc608a.h b/main/atecc608a.h new file mode 100644 index 0000000..e4ff74f --- /dev/null +++ b/main/atecc608a.h @@ -0,0 +1,418 @@ +#ifndef ATECC608B_H +#define ATECC608B_H + +#include "cryptoauthlib.h" +#include "sdkconfig.h" +#include +#include +#include +#include + +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; + +/** + * Read ATECC608B configuration zone into global buffer + */ +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; +} + +/** + * Print ATECC608B configuration zone to console with detailed subzone breakdown + * According to ATECC608B Table 2-4 + */ +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"); +} + +/** + * Get ATECC608B configuration as JSON string (caller must free the returned + * string) + */ +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; + } + + 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 diff --git a/main/crypto.h b/main/crypto.h new file mode 100644 index 0000000..9c576c1 --- /dev/null +++ b/main/crypto.h @@ -0,0 +1,306 @@ +#ifndef CRYPTO_H +#define CRYPTO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG_CRYPTO = "crypto"; + +// --- Constants --- + +// P-256 uses 256 bits = 32 bytes per coordinate +const int P256_PRIVATE_KEY_SIZE = 32; // Scalar value +const int P256_PUBLIC_KEY_SIZE = 64; // Uncompressed point (x + y) +const int P256_COORDINATE_SIZE = 32; // Single coordinate (x or y) +const int GCM_TAG_SIZE = 16; +const int SALT_SIZE = 16; +const int PBKDF2_ITERATIONS = 1000; + +// --- RNG Management --- +class RNG { +private: + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + bool initialized; + +public: + RNG() : initialized(false) {} + + ~RNG() { cleanup(); } + + int init() { + if (initialized) + return 0; + + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&ctr_drbg); + + const char *pers = "esp32_tang_server"; + int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, + (const unsigned char *)pers, strlen(pers)); + if (ret != 0) + return ret; + + initialized = true; + return 0; + } + + void cleanup() { + if (initialized) { + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + initialized = false; + } + } + + mbedtls_ctr_drbg_context *context() { return &ctr_drbg; } +}; + +// Global RNG instance +static RNG global_rng; + +// --- P-256 EC Operations --- +class P256 { +public: + static bool generate_keypair(uint8_t *pub_key, uint8_t *priv_key) { + int ret = global_rng.init(); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "RNG init failed: -0x%04x", -ret); + return false; + } + + mbedtls_ecp_group grp; + mbedtls_ecp_point Q; + mbedtls_mpi d; + + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&Q); + mbedtls_mpi_init(&d); + + ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "ECP group load failed: -0x%04x", -ret); + } else { + ret = mbedtls_ecp_gen_keypair(&grp, &d, &Q, mbedtls_ctr_drbg_random, + global_rng.context()); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "ECP keypair gen failed: -0x%04x", -ret); + } + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&d, priv_key, P256_COORDINATE_SIZE); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "Write private key failed: -0x%04x", -ret); + } + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key, + P256_COORDINATE_SIZE); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "Write pub key X failed: -0x%04x", -ret); + } + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y), + pub_key + P256_COORDINATE_SIZE, + P256_COORDINATE_SIZE); + if (ret != 0) { + ESP_LOGE(TAG_CRYPTO, "Write pub key Y failed: -0x%04x", -ret); + } + } + + mbedtls_ecp_group_free(&grp); + mbedtls_ecp_point_free(&Q); + mbedtls_mpi_free(&d); + + return (ret == 0); + } + + static bool compute_public_key(const uint8_t *priv_key, uint8_t *pub_key) { + if (global_rng.init() != 0) + return false; + + mbedtls_ecp_group grp; + mbedtls_ecp_point Q; + mbedtls_mpi d; + + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&Q); + mbedtls_mpi_init(&d); + + int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_ecp_mul(&grp, &Q, &d, &grp.G, mbedtls_ctr_drbg_random, + global_rng.context()); + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key, + P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y), + pub_key + P256_COORDINATE_SIZE, + P256_COORDINATE_SIZE); + } + + mbedtls_ecp_group_free(&grp); + mbedtls_ecp_point_free(&Q); + mbedtls_mpi_free(&d); + + return (ret == 0); + } + + static bool ecdh_compute_shared_point(const uint8_t *peer_pub_key, + const uint8_t *priv_key, + uint8_t *shared_point, + bool full_point = true) { + if (global_rng.init() != 0) + return false; + + mbedtls_ecp_group grp; + mbedtls_ecp_point Q; + mbedtls_mpi d; + + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&Q); + mbedtls_mpi_init(&d); + + int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(X), peer_pub_key, + P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(Y), + peer_pub_key + P256_COORDINATE_SIZE, + P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_mpi_lset(&Q.MBEDTLS_PRIVATE(Z), 1); + } + if (ret == 0) { + ret = mbedtls_ecp_check_pubkey(&grp, &Q); + } + if (ret == 0) { + ret = mbedtls_ecp_mul(&grp, &Q, &d, &Q, mbedtls_ctr_drbg_random, + global_rng.context()); + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), shared_point, + P256_COORDINATE_SIZE); + } + if (ret == 0 && full_point) { + ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y), + shared_point + P256_COORDINATE_SIZE, + P256_COORDINATE_SIZE); + } + + mbedtls_ecp_group_free(&grp); + mbedtls_ecp_point_free(&Q); + mbedtls_mpi_free(&d); + + return (ret == 0); + } + + static bool sign(const uint8_t *hash, size_t hash_len, + const uint8_t *priv_key, uint8_t *signature) { + if (global_rng.init() != 0) + return false; + + mbedtls_ecp_group grp; + mbedtls_mpi d, r, s; + + mbedtls_ecp_group_init(&grp); + mbedtls_mpi_init(&d); + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + + int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_ecdsa_sign(&grp, &r, &s, &d, hash, hash_len, + mbedtls_ctr_drbg_random, global_rng.context()); + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&r, signature, P256_COORDINATE_SIZE); + } + if (ret == 0) { + ret = mbedtls_mpi_write_binary(&s, signature + P256_COORDINATE_SIZE, + P256_COORDINATE_SIZE); + } + + mbedtls_ecp_group_free(&grp); + mbedtls_mpi_free(&d); + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + + return (ret == 0); + } +}; + +// --- AES-GCM Operations --- +class AESGCM { +public: + static bool encrypt(uint8_t *plaintext, size_t len, const uint8_t *key, + size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *aad, size_t aad_len, uint8_t *tag) { + mbedtls_gcm_context ctx; + mbedtls_gcm_init(&ctx); + + int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8); + if (ret == 0) { + ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, len, iv, + iv_len, aad, aad_len, plaintext, + plaintext, GCM_TAG_SIZE, tag); + } + + mbedtls_gcm_free(&ctx); + return (ret == 0); + } + + static bool decrypt(uint8_t *ciphertext, size_t len, const uint8_t *key, + size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *aad, size_t aad_len, const uint8_t *tag) { + mbedtls_gcm_context ctx; + mbedtls_gcm_init(&ctx); + + int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8); + if (ret == 0) { + ret = mbedtls_gcm_auth_decrypt(&ctx, len, iv, iv_len, aad, aad_len, tag, + GCM_TAG_SIZE, ciphertext, ciphertext); + } + + mbedtls_gcm_free(&ctx); + return (ret == 0); + } +}; + +// --- PBKDF2 --- +class PBKDF2 { +public: + static int derive_key(uint8_t *output, size_t key_len, const char *password, + const uint8_t *salt, size_t salt_len, int iterations) { + return mbedtls_pkcs5_pbkdf2_hmac_ext( + MBEDTLS_MD_SHA256, (const uint8_t *)password, strlen(password), salt, + salt_len, iterations, key_len, output); + } +}; + +#endif // CRYPTO_H diff --git a/main/encoding.h b/main/encoding.h new file mode 100644 index 0000000..e08c491 --- /dev/null +++ b/main/encoding.h @@ -0,0 +1,30 @@ +#ifndef ENCODING_H +#define ENCODING_H + +#include +#include +#include +#include + +static bool b64url_encode_buf(const uint8_t *data, size_t data_len, + char *out_buf, size_t out_max_len) { + size_t b64_len = out_max_len; + ATCA_STATUS status = atcab_base64encode_(data, data_len, out_buf, &b64_len, + atcab_b64rules_urlsafe()); + + return (status == ATCA_SUCCESS && b64_len < out_max_len); +} + +static bool b64url_decode_buf(const char *in_str, uint8_t *out_buf, + size_t expected_len) { + if (!in_str || !out_buf) + return false; + + size_t out_len = expected_len; + ATCA_STATUS status = atcab_base64decode_(in_str, strlen(in_str), out_buf, + &out_len, atcab_b64rules_urlsafe()); + + return (status == ATCA_SUCCESS && out_len == expected_len); +} + +#endif // ENCODING_H \ No newline at end of file diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..4fa4249 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,6 @@ +--- +dependencies: + idf: + version: ">=4.1.0" + esp-cryptoauthlib: + git: https://github.com/espressif/esp-cryptoauthlib.git diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..c5832d2 --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,12 @@ +/* + * ESP32 Tang Server - Main Entry Point + */ +#include "TangServer.h" + +extern "C" void app_main(void) { + setup(); + + while (true) { + loop(); + } +} diff --git a/main/provision.h b/main/provision.h new file mode 100644 index 0000000..19229a7 --- /dev/null +++ b/main/provision.h @@ -0,0 +1,357 @@ +#ifndef PROVISION_H +#define PROVISION_H + +#include "cryptoauthlib.h" +#include +#include +#include + +static const char *TAG_PROVISION = "provision"; + +// Hardcoded HMAC key for prototyping (32 bytes) +// WARNING: This is for prototyping only! In production, this should be securely +// generated +static const uint8_t IO_KEY[32] = { + 0x2c, 0x43, 0x34, 0x9c, 0x4c, 0xe6, 0x70, 0xf3, 0xcb, 0x10, 0xef, + 0xcc, 0x56, 0xf0, 0xd0, 0xc4, 0x03, 0x2c, 0x45, 0x9f, 0xf3, 0xcb, + 0x29, 0x27, 0x22, 0x8e, 0x93, 0x3c, 0xfe, 0x6e, 0x87, 0xed}; + +static const uint8_t HMAC_KEY[32] = { + 0x24, 0x01, 0x2a, 0xf7, 0x3e, 0x62, 0x7a, 0x5e, 0x5e, 0xdc, 0xf0, + 0xce, 0xd6, 0xe5, 0x32, 0x20, 0x56, 0xca, 0x29, 0xd1, 0x52, 0xf8, + 0x17, 0x23, 0x06, 0x75, 0x4f, 0x1d, 0xb9, 0x85, 0x51, 0x5e}; + +// ATECC608B Configuration values +// SlotConfig (Bytes 20-51): 32 bytes for 16 slots (2 bytes each) +static const uint8_t ATECC_SLOT_CONFIG[32] = { + 0x81, 0x00, 0x81, 0x20, 0x81, 0x20, 0x81, 0x20, 0x84, 0x20, 0x84, + 0x20, 0xc6, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9a, 0x4a, + 0x84, 0x20, 0x84, 0x20, 0x9a, 0x4a, 0x00, 0x00, 0x00, 0x00}; + +// KeyConfig (Bytes 96-127): 32 bytes for 16 slots (2 bytes each) +static const uint8_t ATECC_KEY_CONFIG[32] = { + 0x33, 0x00, 0x13, 0x00, 0xd3, 0x09, 0x93, 0x0a, 0x53, 0x00, 0x53, + 0x10, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x5c, 0x00, + 0x53, 0x00, 0x53, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// Byte 69: Persistent Latch enabled with Keyslot 10 +static const uint8_t ATECC_LAST_KEY_USE_10 = 0x8a; + +// Bytes 90-91: IO Protection optional and Key is Slot 6 +static const uint8_t ATECC_CHIP_OPTIONS[2] = {0x02, 0x60}; + +/** + * Check if ATECC608B config zone is locked + * Returns: true if locked, false if unlocked + */ +bool is_atecc608b_config_locked() { + extern uint8_t g_atecc_config_data[128]; + extern bool g_atecc_config_valid; + + // Read config if not already read + if (!g_atecc_config_valid) { + extern bool atecc608B_read_config(); + if (!atecc608B_read_config()) { + ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config"); + return false; // Assume unlocked if we can't read + } + } + + // Byte 87: LockConfig (Config Zone Lock) + // 0x00 = LOCKED, any other value = UNLOCKED + bool locked = (g_atecc_config_data[87] == 0x00); + ESP_LOGI(TAG_PROVISION, "ATECC608B Config Zone: %s (0x%02X)", + locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[87]); + return locked; +} + +/** + * Check if ATECC608B data zone is locked + * Returns: true if locked, false if unlocked + */ +bool is_atecc608b_data_locked() { + extern uint8_t g_atecc_config_data[128]; + extern bool g_atecc_config_valid; + + // Read config if not already read + if (!g_atecc_config_valid) { + extern bool atecc608B_read_config(); + if (!atecc608B_read_config()) { + ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config"); + return false; // Assume unlocked if we can't read + } + } + + // Byte 86: LockValue (Data/OTP Zone Lock) + // 0x00 = LOCKED, any other value = UNLOCKED + bool locked = (g_atecc_config_data[86] == 0x00); + ESP_LOGI(TAG_PROVISION, "ATECC608B Data Zone: %s (0x%02X)", + locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[86]); + return locked; +} + +/** + * Check if ESP32-C6 efuse BLOCK_KEY5 is already used + * Returns: true if used, false if not used + */ +bool is_efuse_key5_used() { + // Read the key purpose for BLOCK_KEY5 + esp_efuse_purpose_t purpose; + esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_KEY_PURPOSE_5, &purpose, + sizeof(purpose) * 8); + + if (err != ESP_OK) { + ESP_LOGE(TAG_PROVISION, "Failed to read KEY_PURPOSE_5: %s", + esp_err_to_name(err)); + return false; + } + + ESP_LOGI(TAG_PROVISION, "EFUSE KEY_PURPOSE_5: %d (0=NONE/unused)", purpose); + + // Purpose 0 (ESP_EFUSE_KEY_PURPOSE_USER) typically means unused/default + // Any non-zero value means it's configured for something + return (purpose != ESP_EFUSE_KEY_PURPOSE_USER); +} + +/** + * Provision the ATECC608B configuration and lock the config zone + * Returns: true on success, false on failure + */ +bool provision_atecc608b_config() { + ESP_LOGI(TAG_PROVISION, + "=== Starting ATECC608B Configuration Provisioning ==="); + + // Check if config zone is already locked + if (is_atecc608b_config_locked()) { + ESP_LOGW(TAG_PROVISION, "Config zone is already locked!"); + return false; + } + + // Wake the device + ATCA_STATUS status = atcab_wakeup(); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed to wake ATECC608B: 0x%02X", status); + return false; + } + + // 1. Ensure we have the current 128-byte config read into memory + extern uint8_t g_atecc_config_data[128]; + extern bool g_atecc_config_valid; + + if (!g_atecc_config_valid) { + ESP_LOGI(TAG_PROVISION, "Reading full config zone into memory..."); + status = atcab_read_config_zone(g_atecc_config_data); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed to read config zone: 0x%02X", status); + return false; + } + g_atecc_config_valid = true; + } + + // 2. Modify the in-memory array with our desired settings + ESP_LOGI(TAG_PROVISION, "Applying configuration changes to memory..."); + memcpy(&g_atecc_config_data[20], ATECC_SLOT_CONFIG, 32); // SlotConfig + g_atecc_config_data[69] = ATECC_LAST_KEY_USE_10; // Persistent Latch + memcpy(&g_atecc_config_data[90], ATECC_CHIP_OPTIONS, 2); // ChipOptions + memcpy(&g_atecc_config_data[96], ATECC_KEY_CONFIG, 32); // KeyConfig + + // 3. Write the whole modified config zone back to the device + // atcab_write_config_zone safely skips the read-only bytes (0-15) + ESP_LOGI(TAG_PROVISION, "Writing modified config zone back to ATECC608B..."); + status = atcab_write_config_zone(g_atecc_config_data); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed to write config zone: 0x%02X", status); + return false; + } + ESP_LOGI(TAG_PROVISION, "Configuration written successfully"); + + // 4. Lock the config zone + ESP_LOGI(TAG_PROVISION, "Locking config zone..."); + status = atcab_lock_config_zone(); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed to lock config zone: 0x%02X", status); + return false; + } + + ESP_LOGI(TAG_PROVISION, + "ATECC608B configuration provisioned and locked successfully!"); + + // Invalidate cached config so it gets accurately re-read on next use + g_atecc_config_valid = false; + + // Verify it was locked + if (is_atecc608b_config_locked()) { + ESP_LOGI(TAG_PROVISION, "Verification: Config zone is now locked"); + return true; + } else { + ESP_LOGE(TAG_PROVISION, + "Verification failed: Config zone still appears unlocked"); + return false; + } +} + +/** + * Provision the ATECC608B Data Zone with required keys + * Returns: true on success, false on failure + */ +bool provision_atecc608b_data_zone() { + ESP_LOGI(TAG_PROVISION, "=== Starting ATECC608B Data Zone Provisioning ==="); + + // 1. Prerequisites check + if (is_atecc608b_data_locked()) { + ESP_LOGW(TAG_PROVISION, + "Data zone is already locked! Cannot provision keys."); + return false; + } + + if (!is_atecc608b_config_locked()) { + ESP_LOGE(TAG_PROVISION, + "Config zone MUST be locked before generating keys (GenKey)."); + return false; + } + + ATCA_STATUS status; + + // 2. Write Symmetric Keys (Slots 6, 10, 13) + ESP_LOGI(TAG_PROVISION, "Writing IO_KEY to Slot 6..."); + status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 6, 0, IO_KEY, 32); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed Slot 6: 0x%02X", status); + return false; + } + + ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 10..."); + status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 10, 0, HMAC_KEY, 32); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed Slot 10: 0x%02X", status); + return false; + } + + ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 13..."); + status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 13, 0, HMAC_KEY, 32); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed Slot 13: 0x%02X", status); + return false; + } + + // 3. Write ECC Public Key to Slot 9 + // Note: The 0x04 prefix byte is stripped. The ATECC608B expects exactly 64 + // bytes. + static const uint8_t SLOT9_PUB_KEY[64] = { + 0x76, 0xc1, 0xa2, 0xe9, 0x63, 0xda, 0x58, 0x41, 0x12, 0x4e, 0xe7, + 0xc5, 0x3b, 0xeb, 0x2d, 0xad, 0x72, 0xf4, 0xc4, 0x61, 0xb8, 0x4a, + 0x65, 0xb7, 0xc7, 0x91, 0xdd, 0x59, 0xf9, 0x0a, 0xad, 0xf0, 0x6f, + 0x13, 0xf2, 0xb6, 0x29, 0x05, 0x4f, 0xab, 0x98, 0xdc, 0xfb, 0x93, + 0xab, 0xbd, 0x90, 0xd1, 0xea, 0x92, 0x91, 0x0a, 0xfe, 0x95, 0x7c, + 0xf6, 0xc7, 0x97, 0x41, 0x8e, 0x96, 0x6c, 0xaa, 0x15}; + + ESP_LOGI(TAG_PROVISION, "Writing ECC Public Key to Slot 9..."); + status = atcab_write_pubkey(9, SLOT9_PUB_KEY); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed Slot 9: 0x%02X", status); + return false; + } + + // 4. Generate Random ECC Private Keys (Slots 0-5, 11, 12) + uint8_t pubkey[64]; // Buffer to catch the generated public keys + const uint8_t genkey_slots[] = {0, 1, 2, 3, 4, 5, 11, 12}; + + for (int i = 0; i < sizeof(genkey_slots) / sizeof(genkey_slots[0]); i++) { + uint8_t slot = genkey_slots[i]; + ESP_LOGI(TAG_PROVISION, "Generating ECC Private Key in Slot %d...", slot); + + status = atcab_genkey(slot, pubkey); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed GenKey Slot %d: 0x%02X", slot, status); + return false; + } + + ESP_LOGI(TAG_PROVISION, "Successfully generated Private Key in Slot %d.", + slot); + ESP_LOGI(TAG_PROVISION, "Corresponding Public Key (64 bytes):"); + + // Standard ESP-IDF hex dump (good for reading) + ESP_LOG_BUFFER_HEX(TAG_PROVISION, pubkey, sizeof(pubkey)); + + // Continuous hex string (good for copy-pasting to host/scripts) + printf("--> Copy this public key for Slot %d: 04", + slot); // Prepending the 0x04 uncompressed prefix + for (int j = 0; j < 64; j++) { + printf("%02x", pubkey[j]); + } + printf("\n\n"); + } + + // 5. Lock the Data Zone + ESP_LOGI(TAG_PROVISION, "Locking Data Zone..."); + status = atcab_lock_data_zone(); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG_PROVISION, "Failed to lock Data Zone: 0x%02X", status); + return false; + } + + ESP_LOGI(TAG_PROVISION, + "ATECC608B Data Zone provisioned and locked successfully!"); + return true; +} + +/** + * Check if provisioning is needed + * Returns: true if any condition requires provisioning + */ +bool needs_provisioning() { + bool config_unlocked = !is_atecc608b_config_locked(); + bool data_unlocked = !is_atecc608b_data_locked(); + bool key5_unused = !is_efuse_key5_used(); + + ESP_LOGI(TAG_PROVISION, "Provisioning check:"); + ESP_LOGI(TAG_PROVISION, " - Config unlocked: %s", + config_unlocked ? "YES" : "NO"); + ESP_LOGI(TAG_PROVISION, " - Data unlocked: %s", + data_unlocked ? "YES" : "NO"); + ESP_LOGI(TAG_PROVISION, " - KEY5 unused: %s", key5_unused ? "YES" : "NO"); + ESP_LOGI(TAG_PROVISION, " - Needs provisioning: %s", + (config_unlocked || data_unlocked || key5_unused) ? "YES" : "NO"); + + return (config_unlocked || data_unlocked || key5_unused); +} + +/** + * Provision the ESP32-C6 efuse BLOCK_KEY5 with hardcoded HMAC key + * Returns: true on success, false on failure + */ +bool provision_efuse_key5() { + ESP_LOGI(TAG_PROVISION, "=== Starting EFUSE KEY5 Provisioning ==="); + + // Check if already used + if (is_efuse_key5_used()) { + ESP_LOGW(TAG_PROVISION, "EFUSE KEY5 is already used!"); + return false; + } + + // Write the HMAC key to BLOCK_KEY5 + ESP_LOGI(TAG_PROVISION, "Writing HMAC key to BLOCK_KEY5..."); + esp_err_t err = + esp_efuse_write_key(EFUSE_BLK_KEY5, ESP_EFUSE_KEY_PURPOSE_HMAC_UP, + HMAC_KEY, sizeof(HMAC_KEY)); + + if (err != ESP_OK) { + ESP_LOGE(TAG_PROVISION, "Failed to write EFUSE KEY5: %s", + esp_err_to_name(err)); + return false; + } + + ESP_LOGI(TAG_PROVISION, "EFUSE KEY5 provisioned successfully!"); + ESP_LOGI(TAG_PROVISION, "Purpose set to: HMAC_UP"); + + // Verify it was written + if (is_efuse_key5_used()) { + ESP_LOGI(TAG_PROVISION, "Verification: KEY5 is now marked as used"); + return true; + } else { + ESP_LOGE(TAG_PROVISION, "Verification failed: KEY5 still appears unused"); + return false; + } +} + +#endif // PROVISION_H diff --git a/main/provision_handlers.h b/main/provision_handlers.h new file mode 100644 index 0000000..6e56d52 --- /dev/null +++ b/main/provision_handlers.h @@ -0,0 +1,187 @@ +#ifndef PROVISION_HANDLERS_H +#define PROVISION_HANDLERS_H + +#include "provision.h" +#include "provision_web_page.h" +#include +#include +#include + +static const char *TAG_PROVISION_HANDLERS = "provision_handlers"; + +// Forward declarations +extern httpd_handle_t server_http; + +/** + * Serve the provisioning web page + */ +static esp_err_t handle_provision_page(httpd_req_t *req) { + httpd_resp_set_type(req, "text/html"); + httpd_resp_sendstr(req, PROVISION_WEB_PAGE); + return ESP_OK; +} + +/** + * API endpoint: Get provisioning status + */ +static esp_err_t handle_provision_status(httpd_req_t *req) { + cJSON *response = cJSON_CreateObject(); + + bool config_unlocked = !is_atecc608b_config_locked(); + bool data_unlocked = !is_atecc608b_data_locked(); + bool key5_unused = !is_efuse_key5_used(); + bool needs_prov = needs_provisioning(); + + cJSON_AddBoolToObject(response, "needs_provisioning", needs_prov); + cJSON_AddBoolToObject(response, "config_unlocked", config_unlocked); + cJSON_AddBoolToObject(response, "data_unlocked", data_unlocked); + cJSON_AddBoolToObject(response, "key5_unused", key5_unused); + + char *json_str = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_sendstr(req, json_str); + free(json_str); + + return ESP_OK; +} + +/** + * API endpoint: Provision the device EFUSE + */ +static esp_err_t handle_provision_api(httpd_req_t *req) { + ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning API called"); + + cJSON *response = cJSON_CreateObject(); + bool success = false; + const char *message = "Unknown error"; + + // Check if provisioning is actually needed + if (!needs_provisioning()) { + message = "Device is already provisioned"; + success = true; + ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message); + } else { + bool atecc_done = true; + bool efuse_done = true; + + // Step 1: Provision ATECC608B config if needed + if (!is_atecc608b_config_locked()) { + ESP_LOGI(TAG_PROVISION_HANDLERS, + "Provisioning ATECC608B configuration..."); + atecc_done = provision_atecc608b_config(); + if (!atecc_done) { + message = "Failed to provision ATECC608B configuration"; + success = false; + ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message); + } + } + + // Step 2: Provision EFUSE KEY5 if needed and previous step succeeded + if (atecc_done && !is_efuse_key5_used()) { + ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning EFUSE KEY5..."); + efuse_done = provision_efuse_key5(); + if (!efuse_done) { + message = "Failed to provision EFUSE KEY5"; + success = false; + ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message); + } + } + + if (!is_atecc608b_data_locked()) { + ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning ATECC608B data zone..."); + bool data_done = provision_atecc608b_data_zone(); + if (!data_done) { + message = "Failed to provision ATECC608B data zone"; + success = false; + ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message); + } + } + + // Build success message + if (atecc_done && efuse_done) { + if (!is_atecc608b_config_locked() && !is_efuse_key5_used()) { + message = + "Provisioned ATECC608B configuration and EFUSE KEY5 successfully"; + } else if (!is_atecc608b_config_locked()) { + message = "ATECC608B configuration provisioned and locked successfully"; + } else { + message = "EFUSE KEY5 provisioned successfully with HMAC key"; + } + success = true; + ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message); + } + } + + // Build JSON response + cJSON_AddBoolToObject(response, "success", success); + cJSON_AddStringToObject(response, "message", message); + + char *json_str = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + + // Send response + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_status(req, success ? "200 OK" : "500 Internal Server Error"); + httpd_resp_sendstr(req, json_str); + free(json_str); + + return ESP_OK; +} + +/** + * Handle CORS preflight for provision API + */ +static esp_err_t handle_provision_options(httpd_req_t *req) { + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); + httpd_resp_set_status(req, "204 No Content"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +/** + * Register provisioning handlers + */ +void register_provision_handlers(httpd_handle_t server) { + // Provision page + httpd_uri_t provision_page_uri = {.uri = "/provision", + .method = HTTP_GET, + .handler = handle_provision_page, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &provision_page_uri); + + // API endpoint for getting status + httpd_uri_t provision_status_uri = {.uri = "/api/provision/status", + .method = HTTP_GET, + .handler = handle_provision_status, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &provision_status_uri); + + // API endpoint for provisioning + httpd_uri_t provision_api_uri = {.uri = "/api/provision", + .method = HTTP_POST, + .handler = handle_provision_api, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &provision_api_uri); + + // CORS preflight + httpd_uri_t provision_options_uri = {.uri = "/api/provision", + .method = HTTP_OPTIONS, + .handler = handle_provision_options, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &provision_options_uri); + + ESP_LOGI(TAG_PROVISION_HANDLERS, "Provision routes registered:"); + ESP_LOGI(TAG_PROVISION_HANDLERS, + " GET /provision - Provision page"); + ESP_LOGI(TAG_PROVISION_HANDLERS, + " GET /api/provision/status - Provision status"); + ESP_LOGI(TAG_PROVISION_HANDLERS, " POST /api/provision - Provision API"); +} + +#endif // PROVISION_HANDLERS_H diff --git a/main/provision_web_page.h b/main/provision_web_page.h new file mode 100644 index 0000000..eb4c4d2 --- /dev/null +++ b/main/provision_web_page.h @@ -0,0 +1,323 @@ +#ifndef PROVISION_WEB_PAGE_H +#define PROVISION_WEB_PAGE_H + +// Embedded web interface for device provisioning + +const char PROVISION_WEB_PAGE[] = R"rawliteral( + + + + + + ESP32 Provisioning + + + +
+
+

🔧 Device Provisioning Required

+

Your device needs to be provisioned before first use

+
+ +
+

⚠️ Provisioning Needed

+
    +
  • Loading status...
  • +
+
+ +
+

ℹ️ What This Does

+

+ This will provision your device security hardware: +

+
    +
  • ATECC608B: Write secure configuration (SlotConfig, KeyConfig, ChipOptions) and lock the config zone
  • +
  • ESP32-C6: Write hardcoded HMAC key to EFUSE BLOCK_KEY5 with purpose EFUSE_KEY_PURPOSE_HMAC_UP
  • +
+

+ ⚠️ Warning: These are one-time operations and cannot be undone! +

+
+ +
+ Device + + + +
+ + + + +)rawliteral"; + +#endif // PROVISION_WEB_PAGE_H diff --git a/main/tang_handlers.h b/main/tang_handlers.h new file mode 100644 index 0000000..19a72e3 --- /dev/null +++ b/main/tang_handlers.h @@ -0,0 +1,288 @@ +#ifndef TANG_HANDLERS_H +#define TANG_HANDLERS_H + +#include "crypto.h" +#include "cryptoauthlib.h" +#include "encoding.h" +#include "tang_storage.h" +#include +#include +#include +#include +#include + +static const char *TAG_HANDLERS = "tang_handlers"; + +// Forward declarations +extern httpd_handle_t server_http; +extern TangKeyStore keystore; +extern bool unlocked; + +// GET /adv - Advertisement endpoint (signed JWK set) +static esp_err_t handle_adv(httpd_req_t *req) { + if (!unlocked) { + httpd_resp_set_status(req, "503 Service Unavailable"); + httpd_resp_sendstr(req, "Server not active"); + return ESP_FAIL; + } + + uint8_t sig_pub[ATCA_PUB_KEY_SIZE]; + + if (atcab_get_pubkey(1, sig_pub) != ATCA_SUCCESS) { + ESP_LOGE(TAG_HANDLERS, "Failed to read public keys from secure element"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Hardware error"); + return ESP_FAIL; + } + + char sig_x_b64[64] = {0}, sig_y_b64[64] = {0}; + char rec_x_b64[64] = {0}, rec_y_b64[64] = {0}; + + b64url_encode_buf(&sig_pub[0], 32, sig_x_b64, sizeof(sig_x_b64)); + b64url_encode_buf(&sig_pub[32], 32, sig_y_b64, sizeof(sig_y_b64)); + b64url_encode_buf(&keystore.exc_pub[0], 32, rec_x_b64, sizeof(rec_x_b64)); + b64url_encode_buf(&keystore.exc_pub[32], 32, rec_y_b64, sizeof(rec_y_b64)); + + // Build JWK set payload using cJSON + cJSON *payload_root = cJSON_CreateObject(); + cJSON *keys = cJSON_CreateArray(); + + // Signing/verification key + cJSON *sig_key = cJSON_CreateObject(); + cJSON_AddStringToObject(sig_key, "kty", "EC"); + cJSON_AddStringToObject(sig_key, "alg", "ES256"); + cJSON_AddStringToObject(sig_key, "crv", "P-256"); + cJSON_AddStringToObject(sig_key, "x", sig_x_b64); + cJSON_AddStringToObject(sig_key, "y", sig_y_b64); + + cJSON *sig_key_ops = cJSON_CreateArray(); + cJSON_AddItemToArray(sig_key_ops, cJSON_CreateString("verify")); + cJSON_AddItemToObject(sig_key, "key_ops", sig_key_ops); + cJSON_AddItemToArray(keys, sig_key); + + cJSON *rec_key = cJSON_CreateObject(); + cJSON_AddStringToObject(rec_key, "alg", "ECMR"); + cJSON_AddStringToObject(rec_key, "kty", "EC"); + cJSON_AddStringToObject(rec_key, "crv", "P-256"); + cJSON_AddStringToObject(rec_key, "x", rec_x_b64); + cJSON_AddStringToObject(rec_key, "y", rec_y_b64); + cJSON *rec_key_ops = cJSON_CreateArray(); + cJSON_AddItemToArray(rec_key_ops, cJSON_CreateString("deriveKey")); + cJSON_AddItemToObject(rec_key, "key_ops", rec_key_ops); + cJSON_AddItemToArray(keys, rec_key); + + cJSON_AddItemToObject(payload_root, "keys", keys); + + // Print payload JSON and encode to B64 dynamically + char *payload_json = cJSON_PrintUnformatted(payload_root); + size_t payload_len = strlen(payload_json); + size_t payload_b64_size = ((payload_len + 2) / 3) * 4 + 1; + char *payload_b64 = (char *)malloc(payload_b64_size); + b64url_encode_buf((uint8_t *)payload_json, payload_len, payload_b64, + payload_b64_size); + + free(payload_json); + cJSON_Delete(payload_root); + + // Create protected header + cJSON *protected_root = cJSON_CreateObject(); + cJSON_AddStringToObject(protected_root, "alg", "ES256"); + cJSON_AddStringToObject(protected_root, "cty", "jwk-set+json"); + + char *protected_json = cJSON_PrintUnformatted(protected_root); + size_t protected_len = strlen(protected_json); + size_t protected_b64_size = ((protected_len + 2) / 3) * 4 + 1; + char *protected_b64 = (char *)malloc(protected_b64_size); + b64url_encode_buf((uint8_t *)protected_json, protected_len, protected_b64, + protected_b64_size); + + free(protected_json); + cJSON_Delete(protected_root); + + // Sign the payload + size_t signing_input_size = + strlen(protected_b64) + 1 + strlen(payload_b64) + 1; + char *signing_input = (char *)malloc(signing_input_size); + snprintf(signing_input, signing_input_size, "%s.%s", protected_b64, + payload_b64); + + uint8_t hash[32]; + mbedtls_sha256((const uint8_t *)signing_input, strlen(signing_input), hash, + 0); + free(signing_input); + + uint8_t signature[ATCA_ECCP256_SIG_SIZE]; + if (atcab_sign(1, hash, signature) != ATCA_SUCCESS) { + ESP_LOGE(TAG_HANDLERS, "Hardware signing failed"); + free(payload_b64); + free(protected_b64); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Signing failed"); + return ESP_FAIL; + } + + char sig_b64[128] = {0}; + b64url_encode_buf(signature, sizeof(signature), sig_b64, sizeof(sig_b64)); + + cJSON *jws_root = cJSON_CreateObject(); + cJSON_AddStringToObject(jws_root, "payload", payload_b64); + cJSON_AddStringToObject(jws_root, "protected", protected_b64); + cJSON_AddStringToObject(jws_root, "signature", sig_b64); + + char *response = cJSON_PrintUnformatted(jws_root); + cJSON_Delete(jws_root); + + free(payload_b64); + free(protected_b64); + + httpd_resp_set_type( + req, "application/jose+json"); // Tang expects this specific type + httpd_resp_sendstr(req, response); + free(response); + + ESP_LOGI(TAG_HANDLERS, "Served /adv"); + return ESP_OK; +} + +// POST /rec or /rec/{kid} - Recovery endpoint +static esp_err_t handle_rec(httpd_req_t *req) { + if (!unlocked) { + httpd_resp_set_status(req, "503 Service Unavailable"); + httpd_resp_sendstr(req, "Server not active"); + return ESP_FAIL; + } + + // Read request body + char *buf = (char *)malloc(req->content_len + 1); + if (!buf) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Memory allocation failed"); + return ESP_FAIL; + } + + int ret = httpd_req_recv(req, buf, req->content_len); + if (ret <= 0) { + free(buf); + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + buf[ret] = '\0'; + + // Parse JSON + cJSON *req_doc = cJSON_Parse(buf); + free(buf); + + if (!req_doc) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + // Extract client's ephemeral public key + cJSON *x_item = cJSON_GetObjectItem(req_doc, "x"); + cJSON *y_item = cJSON_GetObjectItem(req_doc, "y"); + + if (!x_item || !y_item || !cJSON_IsString(x_item) || + !cJSON_IsString(y_item)) { + cJSON_Delete(req_doc); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, + "Missing x or y coordinates"); + return ESP_FAIL; + } + + uint8_t client_pub_key[P256_PUBLIC_KEY_SIZE]; + + // Use the new helper to decode and strictly validate the length of both + // coordinates + if (!b64url_decode_buf(x_item->valuestring, &client_pub_key[0], + P256_COORDINATE_SIZE) || + !b64url_decode_buf(y_item->valuestring, + &client_pub_key[P256_COORDINATE_SIZE], + P256_COORDINATE_SIZE)) { + cJSON_Delete(req_doc); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid key coordinates"); + return ESP_FAIL; + } + + cJSON_Delete(req_doc); + + // Perform ECDH to get shared point + uint8_t shared_point[P256_PUBLIC_KEY_SIZE]; + if (!P256::ecdh_compute_shared_point(client_pub_key, keystore.exc_priv, + shared_point, true)) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "ECDH computation failed"); + return ESP_FAIL; + } + + char shared_x_b64[64] = {0}; + char shared_y_b64[64] = {0}; + + b64url_encode_buf(&shared_point[0], P256_COORDINATE_SIZE, shared_x_b64, + sizeof(shared_x_b64)); + b64url_encode_buf(&shared_point[P256_COORDINATE_SIZE], P256_COORDINATE_SIZE, + shared_y_b64, sizeof(shared_y_b64)); + + // Return shared point as JWK + cJSON *resp_root = cJSON_CreateObject(); + cJSON_AddStringToObject(resp_root, "alg", "ECMR"); + cJSON_AddStringToObject(resp_root, "kty", "EC"); + cJSON_AddStringToObject(resp_root, "crv", "P-256"); + cJSON_AddStringToObject(resp_root, "x", shared_x_b64); + cJSON_AddStringToObject(resp_root, "y", shared_y_b64); + cJSON *key_ops = cJSON_CreateArray(); + cJSON_AddItemToArray(key_ops, cJSON_CreateString("deriveKey")); + cJSON_AddItemToObject(resp_root, "key_ops", key_ops); + + char *response = cJSON_PrintUnformatted(resp_root); + cJSON_Delete(resp_root); + + httpd_resp_set_type(req, "application/jose+json"); + httpd_resp_sendstr(req, response); + free(response); + + ESP_LOGI(TAG_HANDLERS, "Served %s", req->uri); + return ESP_OK; +} + +// --- Administration Handlers --- + +// GET /config - Get ATECC608B configuration +static esp_err_t handle_config(httpd_req_t *req) { + ESP_LOGI(TAG_HANDLERS, "Serving ATECC608B config"); + + char *json_str = atecc608B_get_config_json(); + + if (!json_str) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Config data not available"); + return ESP_FAIL; + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, json_str); + free(json_str); + + ESP_LOGI(TAG_HANDLERS, "Served /config"); + return ESP_OK; +} + +// GET /reboot - Reboot device +static esp_err_t handle_reboot(httpd_req_t *req) { + httpd_resp_sendstr(req, "Rebooting..."); + vTaskDelay(pdMS_TO_TICKS(1000)); + esp_restart(); + return ESP_OK; +} + +// 404 handler - Handle /rec/{kid} by routing to handle_rec +static esp_err_t handle_not_found(httpd_req_t *req, httpd_err_code_t err) { + // Check if it's a POST to /rec/{kid} + if (req->method == HTTP_POST && strncmp(req->uri, "/rec/", 5) == 0) { + return handle_rec(req); + } + + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not found"); + return ESP_FAIL; +} + +#endif // TANG_HANDLERS_H diff --git a/main/tang_storage.h b/main/tang_storage.h new file mode 100644 index 0000000..a1568cc --- /dev/null +++ b/main/tang_storage.h @@ -0,0 +1,87 @@ +#ifndef TANG_STORAGE_H +#define TANG_STORAGE_H + +#include "crypto.h" +#include "encoding.h" +#include +#include +#include +#include +#include + +static const char *TAG_STORAGE = "tang_storage"; + +// --- Key Storage & Management --- +class TangKeyStore { +private: + // No longer need salt for password-based encryption + +public: + // Tang server keys (encrypted at rest, decrypted in memory when active) + uint8_t exc_priv[P256_PRIVATE_KEY_SIZE]; + uint8_t exc_pub[P256_PUBLIC_KEY_SIZE]; + + // Admin key (persistent) + uint8_t admin_priv[P256_PRIVATE_KEY_SIZE]; + uint8_t admin_pub[P256_PUBLIC_KEY_SIZE]; + + bool is_configured() { + nvs_handle_t handle; + esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle); + if (err != ESP_OK) { + return false; + } + + size_t required_size = 0; + err = nvs_get_blob(handle, "admin_key", nullptr, &required_size); + bool configured = (err == ESP_OK && required_size == P256_PRIVATE_KEY_SIZE); + + nvs_close(handle); + return configured; + } + + // Save Tang keys directly to NVS (no encryption) + bool save_tang_keys() { + nvs_handle_t handle; + esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err)); + return false; + } + + // Save exchange key + err = nvs_set_blob(handle, "tang_exc_key", exc_priv, P256_PRIVATE_KEY_SIZE); + if (err != ESP_OK) { + nvs_close(handle); + return false; + } + + err = nvs_commit(handle); + nvs_close(handle); + return (err == ESP_OK); + } + + // Load Tang keys directly from NVS (no decryption) + bool load_tang_keys() { + nvs_handle_t handle; + esp_err_t err = nvs_open("tang-server", NVS_READONLY, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err)); + return false; + } + + // Load exchange key + size_t len = P256_PRIVATE_KEY_SIZE; + err = nvs_get_blob(handle, "tang_exc_key", exc_priv, &len); + if (err != ESP_OK || len != P256_PRIVATE_KEY_SIZE) { + nvs_close(handle); + return false; + } + P256::compute_public_key(exc_priv, exc_pub); + + nvs_close(handle); + return true; + } +}; + +#endif // TANG_STORAGE_H diff --git a/main/zk_auth.h b/main/zk_auth.h new file mode 100644 index 0000000..edb5471 --- /dev/null +++ b/main/zk_auth.h @@ -0,0 +1,535 @@ +#ifndef ZK_AUTH_H +#define ZK_AUTH_H + +#include "cryptoauthlib.h" +#include "host/atca_host.h" +#include "mbedtls/sha256.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Zero-Knowledge Authentication Module +// Implements Client-Side KDF + ECIES Tunnel for ESP32-C6 +// Format: SHA-256 compatible with ATECC608B +// +// Encryption: AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC) +// - Provides confidentiality (CBC) + authenticity (HMAC) +// - Blob format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80 +// bytes +// - HMAC verified BEFORE decryption (prevents padding oracle attacks) + +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; + bool password_set; + bool unlocked = false; // Set to true after successful authentication + + // 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), password_set(false), unlocked(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; + + // Seed the random number generator + const char *pers = "zk_auth_esp32"; + int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, + (const unsigned char *)pers, strlen(pers)); + if (ret != 0) { + printf("mbedtls_ctr_drbg_seed failed: -0x%04x\n", -ret); + return false; + } + + // Setup ECC group for NIST P-256 (secp256r1) + ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + printf("mbedtls_ecp_group_load failed: -0x%04x\n", -ret); + return false; + } + + // Generate device keypair + ret = mbedtls_ecdh_gen_public(&grp, &device_private_d, &device_public_Q, + mbedtls_ctr_drbg_random, &ctr_drbg); + if (ret != 0) { + printf("mbedtls_ecdh_gen_public failed: -0x%04x\n", -ret); + return false; + } + + // Export public key to uncompressed format (0x04 + X + Y) + size_t olen; + ret = mbedtls_ecp_point_write_binary( + &grp, &device_public_Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, + device_public_key, sizeof(device_public_key)); + if (ret != 0 || olen != 65) { + printf("mbedtls_ecp_point_write_binary failed: -0x%04x\n", -ret); + return false; + } + + initialized = true; + + printf("\n=== ZK Authentication Initialized ===\n"); + printf("Public Key: "); + for (int i = 0; i < 65; i++) { + printf("%02x", device_public_key[i]); + } + printf("\n"); + printf("=====================================\n\n"); + + return true; + } + + // Get device identity (for /api/identity endpoint) + char *get_identity_json() { + char pubkey_hex[131]; // 65 bytes * 2 + null + bin_to_hex(device_public_key, 65, pubkey_hex); + + // Get MAC address to use as salt + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + char mac_hex[13]; // 6 bytes * 2 + null + bin_to_hex(mac, 6, mac_hex); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "pubKey", pubkey_hex); + cJSON_AddStringToObject(root, "macAddress", mac_hex); + + char *json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + return json_str; + } + + void my_host_check_mac(const uint8_t *target_key, const uint8_t *tempkey, + const uint8_t *other_data, const uint8_t *sn, + uint8_t *out_mac) { + // See section 11.2 of the ATECC608B datasheet CheckMac message format + uint8_t msg[88] = {0}; + memcpy(&msg[0], target_key, 32); + memcpy(&msg[32], tempkey, 32); + memcpy(&msg[64], &other_data[0], 4); + memset(&msg[68], 0, 8); + memcpy(&msg[76], &other_data[4], 3); + msg[79] = sn[8]; + memcpy(&msg[80], &other_data[7], 4); + msg[84] = sn[0]; + msg[85] = sn[1]; + memcpy(&msg[86], &other_data[11], 2); + // Hash the exact 88 bytes using the ESP32's hardware accelerator + mbedtls_sha256(msg, sizeof(msg), out_mac, 0); + } + + bool verify_key(const uint8_t *received_key, size_t key_len) { + (void)key_len; + if (received_key == NULL) + return false; + + ATCA_STATUS status; + + bool latch_status = false; + status = atcab_info_get_latch(&latch_status); + + printf("Device Latch Status: %s (0x%02X)\n", + latch_status ? "LATCHED" : "UNLATCHED", latch_status); + + uint8_t sn[ATCA_SERIAL_NUM_SIZE] = {0}; + status = atcab_read_serial_number(sn); + if (status != ATCA_SUCCESS) { + printf("Failed to read Serial Number\n"); + return false; + } + + uint8_t nonce_num_in[20] = {0xAA, 0xBB, 0xCC, 0xDD}; + uint8_t rand_out[32] = {0}; + uint8_t slot10_mac[32] = {0}; + uint8_t other_data_10[13] = "TangActivate"; + struct atca_temp_key temp_key; + memset(&temp_key, 0, sizeof(temp_key)); + + status = atcab_nonce_rand(nonce_num_in, rand_out); + + // Print the nonce output for debugging + if (status != ATCA_SUCCESS) { + printf("Slot 10 Nonce failed: 0x%02X\n", status); + } else { + // Compute the TempKey on the ESP32 + struct atca_nonce_in_out nonce_params; + memset(&nonce_params, 0, sizeof(nonce_params)); + nonce_params.mode = 0x00; + nonce_params.num_in = nonce_num_in; + nonce_params.rand_out = rand_out; + nonce_params.temp_key = &temp_key; + + atcah_nonce(&nonce_params); + + // Host calculates MAC using the calculated TempKey as the challenge + my_host_check_mac(received_key, temp_key.value, other_data_10, sn, + slot10_mac); + + // See section 11.2 of the ATECC608B datasheet CheckMac message format + status = atcab_checkmac(0x01, 10, NULL, slot10_mac, other_data_10); + if (status == ATCA_SUCCESS) { + printf("Slot 10 CheckMac: PASSED\n"); + } else if (status == (ATCA_STATUS)ATCA_CHECKMAC_VERIFY_FAILED) { + // 0xD1 (-47 / 0xFFFFFFD1) see atca_status.h + printf("Slot 10 CheckMac: ACCESS DENIED (Wrong Password)\n"); + } else { + printf("Slot 10 CheckMac: HARDWARE ERROR (0x%02X)\n", status); + } + } + status = atcab_info_set_latch(true); + + if (status != ATCA_SUCCESS) { + printf("Failed to latch device: 0x%02X\n", status); + } + + atcab_info_get_latch(&latch_status); + printf("Device Latch Status after setting: %s (0x%02X)\n", + latch_status ? "LATCHED" : "UNLATCHED", latch_status); + + return (status == ATCA_SUCCESS); + } + + // Process unlock request with ECIES tunnel + char *process_unlock(const char *json_payload, bool *success_out) { + *success_out = false; + + if (!initialized) { + return strdup("{\"error\":\"Not initialized\"}"); + } + + // Parse JSON + cJSON *doc = cJSON_Parse(json_payload); + if (doc == NULL) { + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr != NULL) { + printf("JSON parse error before: %s\n", error_ptr); + } + return strdup("{\"error\":\"Invalid JSON\"}"); + } + + cJSON *client_pub_item = cJSON_GetObjectItem(doc, "clientPub"); + cJSON *encrypted_blob_item = cJSON_GetObjectItem(doc, "blob"); + + const char *client_pub_hex = NULL; + const char *encrypted_blob_hex = NULL; + + if (cJSON_IsString(client_pub_item)) + client_pub_hex = client_pub_item->valuestring; + if (cJSON_IsString(encrypted_blob_item)) + encrypted_blob_hex = encrypted_blob_item->valuestring; + + if (!client_pub_hex || !encrypted_blob_hex) { + cJSON_Delete(doc); + return strdup("{\"error\":\"Missing required fields\"}"); + } + + printf("\n=== Processing Unlock Request ===\n"); + printf("Client Public Key: %s\n", client_pub_hex); + printf("Encrypted Blob: %s\n", encrypted_blob_hex); + + // Convert client public key from hex + size_t client_pub_len = strlen(client_pub_hex) / 2; + uint8_t *client_pub_bin = (uint8_t *)malloc(client_pub_len); + if (!hex_to_bin(client_pub_hex, client_pub_bin, client_pub_len)) { + free(client_pub_bin); + cJSON_Delete(doc); + return strdup("{\"error\":\"Invalid client public key format\"}"); + } + + // Parse client public key point + mbedtls_ecp_point client_point; + mbedtls_ecp_point_init(&client_point); + + int ret = mbedtls_ecp_point_read_binary(&grp, &client_point, client_pub_bin, + client_pub_len); + free(client_pub_bin); + + if (ret != 0) { + mbedtls_ecp_point_free(&client_point); + cJSON_Delete(doc); + printf("Failed to parse client key: -0x%04x\n", -ret); + return strdup("{\"error\":\"Invalid client public key\"}"); + } + + // Compute shared secret using ECDH + mbedtls_mpi shared_secret_mpi; + mbedtls_mpi_init(&shared_secret_mpi); + + ret = mbedtls_ecdh_compute_shared(&grp, &shared_secret_mpi, &client_point, + &device_private_d, + mbedtls_ctr_drbg_random, &ctr_drbg); + + mbedtls_ecp_point_free(&client_point); + + if (ret != 0) { + mbedtls_mpi_free(&shared_secret_mpi); + cJSON_Delete(doc); + printf("ECDH computation failed: -0x%04x\n", -ret); + return strdup("{\"error\":\"ECDH failed\"}"); + } + + // Export shared secret to binary + uint8_t shared_secret_raw[32]; + ret = mbedtls_mpi_write_binary(&shared_secret_mpi, shared_secret_raw, 32); + mbedtls_mpi_free(&shared_secret_mpi); + + if (ret != 0) { + cJSON_Delete(doc); + printf("Shared secret export failed: -0x%04x\n", -ret); + return strdup("{\"error\":\"Shared secret export failed\"}"); + } + + printf("Shared Secret (raw): "); + for (int i = 0; i < 32; i++) + printf("%02x", shared_secret_raw[i]); + printf("\n"); + + // Derive separate keys for encryption and authentication + // This prevents key reuse vulnerabilities + const mbedtls_md_info_t *md_info = + mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + + // Encryption key: SHA256("encryption" || shared_secret) + uint8_t enc_key[32]; + mbedtls_md_context_t md_ctx; + mbedtls_md_init(&md_ctx); + mbedtls_md_setup(&md_ctx, md_info, 0); + mbedtls_md_starts(&md_ctx); + mbedtls_md_update(&md_ctx, (const uint8_t *)"encryption", 10); + mbedtls_md_update(&md_ctx, shared_secret_raw, 32); + mbedtls_md_finish(&md_ctx, enc_key); + + // MAC key: SHA256("authentication" || shared_secret) + uint8_t mac_key[32]; + mbedtls_md_starts(&md_ctx); + mbedtls_md_update(&md_ctx, (const uint8_t *)"authentication", 14); + mbedtls_md_update(&md_ctx, shared_secret_raw, 32); + mbedtls_md_finish(&md_ctx, mac_key); + mbedtls_md_free(&md_ctx); + + printf("Encryption Key: "); + for (int i = 0; i < 32; i++) + printf("%02x", enc_key[i]); + printf("\n"); + + printf("MAC Key: "); + for (int i = 0; i < 32; i++) + printf("%02x", mac_key[i]); + printf("\n"); + + // Securely wipe the raw shared secret + memset(shared_secret_raw, 0, 32); + + // Convert encrypted blob from hex + // Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80 + // bytes + size_t encrypted_len = strlen(encrypted_blob_hex) / 2; + uint8_t *encrypted_blob = (uint8_t *)malloc(encrypted_len); + if (!hex_to_bin(encrypted_blob_hex, encrypted_blob, encrypted_len)) { + free(encrypted_blob); + memset(enc_key, 0, 32); + memset(mac_key, 0, 32); + cJSON_Delete(doc); + return strdup("{\"error\":\"Invalid blob format\"}"); + } + + // Verify blob length: IV(16) + Ciphertext(32) + HMAC(32) = 80 bytes + if (encrypted_len != 80) { + free(encrypted_blob); + memset(enc_key, 0, 32); + memset(mac_key, 0, 32); + cJSON_Delete(doc); + printf("Expected 80 bytes, got %d\n", encrypted_len); + return strdup("{\"error\":\"Invalid blob length\"}"); + } + + // Extract components from blob + uint8_t *iv = encrypted_blob; // First 16 bytes + uint8_t *ciphertext = encrypted_blob + 16; // Next 32 bytes + uint8_t *received_hmac = encrypted_blob + 48; // Last 32 bytes + + printf("IV: "); + for (int i = 0; i < 16; i++) + printf("%02x", iv[i]); + printf("\n"); + + printf("Received HMAC: "); + for (int i = 0; i < 32; i++) + printf("%02x", received_hmac[i]); + printf("\n"); + + // ⚠️ CRITICAL: Verify HMAC BEFORE decrypting + // This prevents padding oracle attacks and ensures data authenticity + uint8_t computed_hmac[32]; + ret = mbedtls_md_hmac(md_info, mac_key, 32, encrypted_blob, 48, + computed_hmac); + + if (ret != 0) { + free(encrypted_blob); + memset(enc_key, 0, 32); + memset(mac_key, 0, 32); + cJSON_Delete(doc); + printf("HMAC computation failed: -0x%04x\n", -ret); + return strdup("{\"error\":\"HMAC computation failed\"}"); + } + + printf("Computed HMAC: "); + for (int i = 0; i < 32; i++) + printf("%02x", computed_hmac[i]); + printf("\n"); + + // Constant-time comparison to prevent timing attacks + int hmac_result = 0; + for (int i = 0; i < 32; i++) { + hmac_result |= received_hmac[i] ^ computed_hmac[i]; + } + + // Wipe MAC key after verification + memset(mac_key, 0, 32); + + if (hmac_result != 0) { + free(encrypted_blob); + memset(enc_key, 0, 32); + cJSON_Delete(doc); + printf("❌ HMAC verification FAILED - ciphertext was modified or wrong " + "key!\n"); + return strdup("{\"error\":\"Authentication failed - data tampered or " + "wrong password\"}"); + } + + printf("✅ HMAC verified - data is authentic\n"); + + // Now safe to decrypt (HMAC passed) + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + + ret = mbedtls_aes_setkey_dec(&aes, enc_key, 256); + if (ret != 0) { + mbedtls_aes_free(&aes); + free(encrypted_blob); + memset(enc_key, 0, 32); + cJSON_Delete(doc); + printf("AES setkey failed: -0x%04x\n", -ret); + return strdup("{\"error\":\"AES setup failed\"}"); + } + + uint8_t *decrypted_data = (uint8_t *)malloc(32); + uint8_t iv_copy[16]; + memcpy(iv_copy, iv, 16); // CBC mode modifies IV + + ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, + 32, // data length + iv_copy, // IV (will be modified) + ciphertext, // input + decrypted_data); // output + + mbedtls_aes_free(&aes); + free(encrypted_blob); + memset(enc_key, 0, 32); + + if (ret != 0) { + free(decrypted_data); + cJSON_Delete(doc); + printf("AES decrypt failed: -0x%04x\n", -ret); + return strdup("{\"error\":\"Decryption failed\"}"); + } + + // Extract the derived key (PBKDF2 hash, 32 bytes) + printf("\n=== DECRYPTED DERIVED KEY ===\n"); + printf("Key (hex): "); + for (size_t i = 0; i < 32; i++) { + printf("%02x", decrypted_data[i]); + } + printf("\n"); + printf("=============================\n\n"); + + // Verify the decrypted key against stored password hash + bool verification_result = verify_key(decrypted_data, 32); + + cJSON *resp_doc = cJSON_CreateObject(); + cJSON_AddBoolToObject(resp_doc, "success", verification_result); + + if (verification_result) { + printf("✅ Password verification SUCCESSFUL\n"); + unlocked = true; + cJSON_AddStringToObject(resp_doc, "message", "Unlock successful"); + } else { + printf("❌ Password verification FAILED\n"); + unlocked = false; + cJSON_AddStringToObject(resp_doc, "error", "Invalid password"); + } + + char *response_str = cJSON_PrintUnformatted(resp_doc); + cJSON_Delete(resp_doc); + cJSON_Delete(doc); + + // Securely wipe decrypted data + memset(decrypted_data, 0, 32); + free(decrypted_data); + + *success_out = verification_result; + return response_str; + } + + // Check if device is unlocked + bool is_unlocked() const { return unlocked; } + + // Lock the device + void lock() { unlocked = false; } +}; + +#endif // ZK_AUTH_H diff --git a/main/zk_handlers.h b/main/zk_handlers.h new file mode 100644 index 0000000..9cb188d --- /dev/null +++ b/main/zk_handlers.h @@ -0,0 +1,159 @@ +#ifndef ZK_HANDLERS_H +#define ZK_HANDLERS_H + +#include "provision.h" +#include "zk_auth.h" +#include "zk_web_page.h" +#include +#include +#include +#include + +static const char *TAG_ZK = "zk_handlers"; + +// Global ZK Auth instance (to be initialized in main) +extern ZKAuth zk_auth; +extern httpd_handle_t server_http; + +// Serve the main web interface +static esp_err_t handle_zk_root(httpd_req_t *req) { + // Check if provisioning is needed - if so, redirect + if (needs_provisioning()) { + ESP_LOGI(TAG_ZK, "Provisioning needed - redirecting to /provision"); + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", "/provision"); + httpd_resp_sendstr(req, "Redirecting to provisioning page..."); + return ESP_OK; + } + + // Normal landing page + httpd_resp_set_type(req, "text/html"); + httpd_resp_sendstr(req, ZK_WEB_PAGE); + return ESP_OK; +} + +// 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 +static esp_err_t handle_zk_unlock(httpd_req_t *req) { + // Read POST body + char content[1024]; + int ret = httpd_req_recv(req, content, sizeof(content) - 1); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + content[ret] = '\0'; + + bool success = false; + char *response = zk_auth.process_unlock(content, &success); + + if (response == NULL) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal error"); + return ESP_FAIL; + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_status(req, success ? "200 OK" : "400 Bad Request"); + httpd_resp_sendstr(req, response); + free(response); + return ESP_OK; +} + +static esp_err_t handle_zk_status(httpd_req_t *req) { + unsigned long uptime_ms = esp_timer_get_time() / 1000; + char response[128]; + snprintf(response, sizeof(response), "{\"unlocked\":%s,\"uptime\":%lu}", + zk_auth.is_unlocked() ? "true" : "false", uptime_ms); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_sendstr(req, response); + return ESP_OK; +} + +// API endpoint: Lock the device +static esp_err_t handle_zk_lock(httpd_req_t *req) { + zk_auth.lock(); + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_sendstr(req, "{\"unlocked\":false}"); + return ESP_OK; +} + +// Handle CORS preflight +static esp_err_t handle_zk_options(httpd_req_t *req) { + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); + httpd_resp_set_status(req, "204 No Content"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +// Register all ZK auth routes to the HTTP server +void register_zk_handlers(httpd_handle_t server) { + // Root handler for ZK web interface + httpd_uri_t root_uri = {.uri = "/", + .method = HTTP_GET, + .handler = handle_zk_root, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &root_uri); + + // API endpoints + 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); + + httpd_uri_t status_uri = {.uri = "/api/status", + .method = HTTP_GET, + .handler = handle_zk_status, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &status_uri); + + httpd_uri_t unlock_uri = {.uri = "/api/unlock", + .method = HTTP_POST, + .handler = handle_zk_unlock, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &unlock_uri); + + httpd_uri_t unlock_options_uri = {.uri = "/api/unlock", + .method = HTTP_OPTIONS, + .handler = handle_zk_options, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &unlock_options_uri); + + httpd_uri_t lock_uri = {.uri = "/api/lock", + .method = HTTP_POST, + .handler = handle_zk_lock, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &lock_uri); + + ESP_LOGI(TAG_ZK, "ZK Auth routes registered:"); + ESP_LOGI(TAG_ZK, " GET / - Web interface"); + ESP_LOGI(TAG_ZK, " GET /api/identity - Device identity"); + ESP_LOGI(TAG_ZK, " GET /api/status - Session status"); + ESP_LOGI(TAG_ZK, " POST /api/unlock - Unlock request"); + ESP_LOGI(TAG_ZK, " POST /api/lock - Lock device"); +} + +#endif // ZK_HANDLERS_H diff --git a/main/zk_web_page.h b/main/zk_web_page.h new file mode 100644 index 0000000..1deea34 --- /dev/null +++ b/main/zk_web_page.h @@ -0,0 +1,833 @@ +#ifndef ZK_WEB_PAGE_H +#define ZK_WEB_PAGE_H + +// Embedded web interface for Zero-Knowledge Authentication +// Includes: HTML, CSS, JavaScript, and crypto libraries (elliptic.js, CryptoJS) + +const char ZK_WEB_PAGE[] = R"rawliteral( + + + + + + ESP32 Zero-Knowledge Auth + + + + +
+
+

Zero-Knowledge Auth

+

ESP32-C6 Secure Unlock

+ +
+ Privacy First: Your password is never transmitted. + The device only receives an encrypted, derived key over an ECIES tunnel. +
+ +
+ + +
+ + + +
+
+ +
+
+
+ + + +
+

Device Unlocked!

+

Authentication successful

+
+ +
+

Session Information

+
+ Status + Active +
+
+ Authentication Method + Password +
+
+ Key Derivation + PBKDF2-SHA256 +
+
+ Encryption + ECIES (P-256 + AES-CBC + HMAC) +
+
+ Authenticated At + -- +
+
+ Device Uptime + -- +
+
+ +
+
+ + + + + + + +)rawliteral"; + +#endif // ZK_WEB_PAGE_H diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..e67c77e --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,13 @@ +# Target Configuration +CONFIG_IDF_TARGET="esp32c6" + +# WiFi Configuration +CONFIG_WIFI_SSID="DoNotSetTheRealValueHere" +CONFIG_WIFI_PASSWORD="PutTheRealPassInTheSdkconfigFile" + +# ATECC608A Configuration +CONFIG_ATCA_I2C_SDA_PIN=22 +CONFIG_ATCA_I2C_SCL_PIN=23 +CONFIG_ATCA_I2C_ADDRESS=0xC0 +CONFIG_ATCA_I2C_BAUD_RATE=1000000 +CONFIG_ATECC608A_TCUSTOM=y