Init
This commit is contained in:
31
.envrc
Normal file
31
.envrc
Normal file
@@ -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"
|
||||||
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@@ -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
|
||||||
17
.vscode/c_cpp_properties.json
vendored
Normal file
17
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"cmake.automaticReconfigure": true,
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||||
|
"python-envs.pythonProjects": []
|
||||||
|
}
|
||||||
11
CMakeLists.txt
Normal file
11
CMakeLists.txt
Normal file
@@ -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})
|
||||||
183
Makefile
Normal file
183
Makefile
Normal file
@@ -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"
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@@ -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://<esp-ip>/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://<esp-ip>/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://<esp-ip>/adv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [Tang Server (reference implementation)](https://github.com/latchset/tang)
|
||||||
21
dependencies.lock
Normal file
21
dependencies.lock
Normal file
@@ -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
|
||||||
117
flake.lock
generated
Normal file
117
flake.lock
generated
Normal file
@@ -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
|
||||||
|
}
|
||||||
189
flake.nix
Normal file
189
flake.nix
Normal file
@@ -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*"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
3
main/CMakeLists.txt
Normal file
3
main/CMakeLists.txt
Normal file
@@ -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)
|
||||||
15
main/Kconfig.projbuild
Normal file
15
main/Kconfig.projbuild
Normal file
@@ -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
|
||||||
241
main/TangServer.h
Normal file
241
main/TangServer.h
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#ifndef TANG_SERVER_H
|
||||||
|
#define TANG_SERVER_H
|
||||||
|
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
#include <esp_event.h>
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <esp_wifi.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/event_groups.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include <nvs_flash.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static const char *TAG = "TangServer";
|
||||||
|
|
||||||
|
// Include core components
|
||||||
|
#include "atecc608a.h"
|
||||||
|
#include "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://<ip>/");
|
||||||
|
ESP_LOGI(TAG, " - Tang Server: http://<ip>/adv");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Loop ---
|
||||||
|
void loop() {
|
||||||
|
// Just delay - HTTP server handles requests in its own task
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // TANG_SERVER_H
|
||||||
418
main/atecc608a.h
Normal file
418
main/atecc608a.h
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
#ifndef ATECC608B_H
|
||||||
|
#define ATECC608B_H
|
||||||
|
|
||||||
|
#include "cryptoauthlib.h"
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
static const char *TAG_ATECC = "ATECC608B";
|
||||||
|
|
||||||
|
// ATECC608B Configuration
|
||||||
|
ATCAIfaceCfg atecc_cfg;
|
||||||
|
|
||||||
|
bool atecc608B_init() {
|
||||||
|
ESP_LOGI(TAG_ATECC, "\n=== ATECC608B Initialization ===");
|
||||||
|
|
||||||
|
// Configure cryptoauthlib for ATECC608B - it will handle I2C initialization
|
||||||
|
atecc_cfg.iface_type = ATCA_I2C_IFACE;
|
||||||
|
atecc_cfg.devtype = ATECC608B;
|
||||||
|
atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS;
|
||||||
|
atecc_cfg.atcai2c.bus = 0; // I2C bus number
|
||||||
|
atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE;
|
||||||
|
atecc_cfg.wake_delay = 1500;
|
||||||
|
atecc_cfg.rx_retries = 20;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG_ATECC,
|
||||||
|
"Configuring ATECC608B: SDA=GPIO%d, SCL=GPIO%d, Address=0x%02X "
|
||||||
|
"(7-bit: 0x%02X)",
|
||||||
|
CONFIG_ATCA_I2C_SDA_PIN, CONFIG_ATCA_I2C_SCL_PIN,
|
||||||
|
CONFIG_ATCA_I2C_ADDRESS, CONFIG_ATCA_I2C_ADDRESS >> 1);
|
||||||
|
|
||||||
|
// Initialize cryptoauthlib (it will initialize I2C internally)
|
||||||
|
ATCA_STATUS status = atcab_init(&atecc_cfg);
|
||||||
|
if (status != ATCA_SUCCESS) {
|
||||||
|
ESP_LOGE(TAG_ATECC, "ERROR: atcab_init failed with status 0x%02X", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG_ATECC, "ATECC608B initialized successfully");
|
||||||
|
|
||||||
|
// Give the device a moment to stabilize
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
|
||||||
|
// Wake the device and test communication
|
||||||
|
status = atcab_wakeup();
|
||||||
|
if (status != ATCA_SUCCESS) {
|
||||||
|
ESP_LOGW(TAG_ATECC, "WARNING: Wake command returned status 0x%02X", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put device to idle
|
||||||
|
atcab_idle();
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global storage for config zone data
|
||||||
|
uint8_t g_atecc_config_data[128] = {0};
|
||||||
|
bool g_atecc_config_valid = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
306
main/crypto.h
Normal file
306
main/crypto.h
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
#ifndef CRYPTO_H
|
||||||
|
#define CRYPTO_H
|
||||||
|
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
#include <mbedtls/ctr_drbg.h>
|
||||||
|
#include <mbedtls/ecdsa.h>
|
||||||
|
#include <mbedtls/ecp.h>
|
||||||
|
#include <mbedtls/entropy.h>
|
||||||
|
#include <mbedtls/gcm.h>
|
||||||
|
#include <mbedtls/md.h>
|
||||||
|
#include <mbedtls/pkcs5.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <mbedtls/sha512.h>
|
||||||
|
|
||||||
|
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
|
||||||
30
main/encoding.h
Normal file
30
main/encoding.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef ENCODING_H
|
||||||
|
#define ENCODING_H
|
||||||
|
|
||||||
|
#include <atca_helpers.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mbedtls/base64.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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
|
||||||
6
main/idf_component.yml
Normal file
6
main/idf_component.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
dependencies:
|
||||||
|
idf:
|
||||||
|
version: ">=4.1.0"
|
||||||
|
esp-cryptoauthlib:
|
||||||
|
git: https://github.com/espressif/esp-cryptoauthlib.git
|
||||||
12
main/main.cpp
Normal file
12
main/main.cpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* ESP32 Tang Server - Main Entry Point
|
||||||
|
*/
|
||||||
|
#include "TangServer.h"
|
||||||
|
|
||||||
|
extern "C" void app_main(void) {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
357
main/provision.h
Normal file
357
main/provision.h
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
#ifndef PROVISION_H
|
||||||
|
#define PROVISION_H
|
||||||
|
|
||||||
|
#include "cryptoauthlib.h"
|
||||||
|
#include <esp_efuse.h>
|
||||||
|
#include <esp_efuse_table.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
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
|
||||||
187
main/provision_handlers.h
Normal file
187
main/provision_handlers.h
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#ifndef PROVISION_HANDLERS_H
|
||||||
|
#define PROVISION_HANDLERS_H
|
||||||
|
|
||||||
|
#include "provision.h"
|
||||||
|
#include "provision_web_page.h"
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
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
|
||||||
323
main/provision_web_page.h
Normal file
323
main/provision_web_page.h
Normal file
@@ -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(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ESP32 Provisioning</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 2px solid #ffc107;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box h3 {
|
||||||
|
color: #856404;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box ul {
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border: 2px solid #2196F3;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box h3 {
|
||||||
|
color: #1976D2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
color: #1565C0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 2px solid #28a745;
|
||||||
|
color: #155724;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 2px solid #dc3545;
|
||||||
|
color: #721c24;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 2px solid #17a2b8;
|
||||||
|
color: #0c5460;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔧 Device Provisioning Required</h1>
|
||||||
|
<p>Your device needs to be provisioned before first use</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning-box">
|
||||||
|
<h3>⚠️ Provisioning Needed</h3>
|
||||||
|
<ul id="provisionReasons">
|
||||||
|
<li>Loading status...</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>ℹ️ What This Does</h3>
|
||||||
|
<p>
|
||||||
|
This will provision your device security hardware:
|
||||||
|
</p>
|
||||||
|
<ul style="margin-top: 10px; margin-left: 20px; color: #1565C0;">
|
||||||
|
<li><strong>ATECC608B:</strong> Write secure configuration (SlotConfig, KeyConfig, ChipOptions) and lock the config zone</li>
|
||||||
|
<li><strong>ESP32-C6:</strong> Write hardcoded HMAC key to EFUSE BLOCK_KEY5 with purpose <code>EFUSE_KEY_PURPOSE_HMAC_UP</code></li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 10px; font-weight: bold;">
|
||||||
|
⚠️ Warning: These are one-time operations and cannot be undone!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statusMessage" class="status-message"></div>
|
||||||
|
Device
|
||||||
|
<button id="provisionBtn" class="provision-btn" onclick="provisionDevice()">
|
||||||
|
Provision EFUSE KEY5
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="skip-link">
|
||||||
|
<a href="/">Skip and continue anyway</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Fetch and display current provisioning status on page load
|
||||||
|
async function loadProvisionStatus() {
|
||||||
|
const reasonsList = document.getElementById('provisionReasons');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/provision/status');
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
// Clear loading message
|
||||||
|
reasonsList.innerHTML = '';
|
||||||
|
|
||||||
|
// Build list of actual conditions
|
||||||
|
|
||||||
|
if (status.config_unlocked) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = 'ATECC608B Config Zone is unlocked';
|
||||||
|
reasonsList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.data_unlocked) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = 'ATECC608B Data Zone is unlocked';
|
||||||
|
reasonsList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.key5_unused) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = 'ESP32-C6 EFUSE BLOCK_KEY5 is not configured';
|
||||||
|
reasonsList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If somehow no conditions are true, show message
|
||||||
|
if (!status.needs_provisioning) {
|
||||||
|
reasonsList.innerHTML = '<li>No provisioning needed (you may skip)</li>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reasonsList.innerHTML = '<li>Error loading status: ' + error.message + '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function provisionDevice() {
|
||||||
|
const btn = document.getElementById('provisionBtn');
|
||||||
|
const statusMsg = document.getElementById('statusMessage');
|
||||||
|
|
||||||
|
// Disable button
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Provisioning...';
|
||||||
|
|
||||||
|
// Show info messageProvisioning device security hardware
|
||||||
|
statusMsg.className = 'status-message info';
|
||||||
|
statusMsg.textContent = 'Writing HMAC key to EFUSE BLOCK_KEY5...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/provision', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
statusMsg.className = 'status-message success';
|
||||||
|
statusMsg.textContent = '✓ ' + result.message;
|
||||||
|
btn.textContent = 'Provisioning Complete!';
|
||||||
|
|
||||||
|
// Redirect to home after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
statusMsg.className = 'status-message error';
|
||||||
|
statusMsg.textContent = '✗ Error: ' + (result.message || 'Unknown error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Retry Provisioning';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
statusMsg.className = 'status-message error';
|
||||||
|
statusMsg.textContent = '✗ Connection error: ' + error.message;
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Retry Provisioning';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load status when page loads
|
||||||
|
window.addEventListener('DOMContentLoaded', loadProvisionStatus);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
#endif // PROVISION_WEB_PAGE_H
|
||||||
288
main/tang_handlers.h
Normal file
288
main/tang_handlers.h
Normal file
@@ -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 <cJSON.h>
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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
|
||||||
87
main/tang_storage.h
Normal file
87
main/tang_storage.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#ifndef TANG_STORAGE_H
|
||||||
|
#define TANG_STORAGE_H
|
||||||
|
|
||||||
|
#include "crypto.h"
|
||||||
|
#include "encoding.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <esp_random.h>
|
||||||
|
#include <nvs.h>
|
||||||
|
#include <nvs_flash.h>
|
||||||
|
|
||||||
|
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
|
||||||
535
main/zk_auth.h
Normal file
535
main/zk_auth.h
Normal file
@@ -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 <cJSON.h>
|
||||||
|
#include <esp_mac.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
#include <mbedtls/aes.h>
|
||||||
|
#include <mbedtls/ctr_drbg.h>
|
||||||
|
#include <mbedtls/ecdh.h>
|
||||||
|
#include <mbedtls/ecp.h>
|
||||||
|
#include <mbedtls/entropy.h>
|
||||||
|
#include <mbedtls/md.h>
|
||||||
|
#include <mbedtls/pkcs5.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// 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
|
||||||
159
main/zk_handlers.h
Normal file
159
main/zk_handlers.h
Normal file
@@ -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 <esp_http_server.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <esp_timer.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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
|
||||||
833
main/zk_web_page.h
Normal file
833
main/zk_web_page.h
Normal file
@@ -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(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ESP32 Zero-Knowledge Auth</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15'/><path d='M17 2v6'/><path d='M17 4h2'/><path d='M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20'/><circle cx='17' cy='10' r='2'/></svg>">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #f0f5f3;
|
||||||
|
border-left: 4px solid #5a7d5a;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: #4a6d4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"],
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e1e8ed;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #5a7d5a;
|
||||||
|
box-shadow: 0 0 0 3px rgba(90, 125, 90, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px rgba(90, 125, 90, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
border: 1px solid #ef5350;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #5a7d5a;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info .label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info .value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-animation {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark-circle {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e8f5e9;
|
||||||
|
position: relative;
|
||||||
|
animation: scaleIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark path {
|
||||||
|
stroke: #2e7d32;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
fill: none;
|
||||||
|
stroke-dasharray: 100;
|
||||||
|
stroke-dashoffset: 100;
|
||||||
|
animation: drawCheck 1s ease-out 1s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drawCheck {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-page {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-page.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlock-page {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlock-page.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #e1e8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e1e8ed;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #d1d8dd;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container" id="mainContainer">
|
||||||
|
<div id="unlockPage" class="unlock-page">
|
||||||
|
<h1><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 8px;"><path d="M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15"/><path d="M17 2v6"/><path d="M17 4h2"/><path d="M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><circle cx="17" cy="10" r="2"/></svg>Zero-Knowledge Auth</h1>
|
||||||
|
<p class="subtitle">ESP32-C6 Secure Unlock</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 2px;"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg> Privacy First:</strong> Your password is never transmitted.
|
||||||
|
The device only receives an encrypted, derived key over an ECIES tunnel.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" placeholder="Enter your password" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" onclick="performSecureUnlock()" id="unlockBtn">
|
||||||
|
Unlock Device
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statusPage" class="status-page">
|
||||||
|
<div class="success-animation">
|
||||||
|
<div class="checkmark-circle">
|
||||||
|
<svg class="checkmark" viewBox="0 0 52 52">
|
||||||
|
<path d="M14 27l7.5 7.5L38 18"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 style="color: #2e7d32; margin-bottom: 10px;">Device Unlocked!</h2>
|
||||||
|
<p style="color: #666;">Authentication successful</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card">
|
||||||
|
<h3>Session Information</h3>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Status</span>
|
||||||
|
<span class="badge badge-success">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Authentication Method</span>
|
||||||
|
<span class="status-value">Password</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Key Derivation</span>
|
||||||
|
<span class="status-value">PBKDF2-SHA256</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Encryption</span>
|
||||||
|
<span class="status-value">ECIES (P-256 + AES-CBC + HMAC)</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Authenticated At</span>
|
||||||
|
<span class="status-value" id="authTime">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Device Uptime</span>
|
||||||
|
<span class="status-value" id="uptime">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" onclick="lockDevice()">
|
||||||
|
Lock Device
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ⚠️ SECURITY WARNING: Password handling in browser - client-side only -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/elliptic/6.5.4/elliptic.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// ===============================================================================
|
||||||
|
// Zero-Knowledge Authentication - Browser-Side Cryptography
|
||||||
|
// ===============================================================================
|
||||||
|
// SECURITY FEATURES IMPLEMENTED:
|
||||||
|
// 1. Password is NEVER transmitted - only PBKDF2-derived hash
|
||||||
|
// 2. Immediate password field clearing after read
|
||||||
|
// 3. Secure memory wiping of all sensitive variables (keys, secrets, hashes)
|
||||||
|
// 4. try-finally blocks ensure cleanup even on errors
|
||||||
|
// 5. Ephemeral ECDH keypair (generated per-session, discarded after)
|
||||||
|
// 6. All sensitive data cleared before function returns
|
||||||
|
//
|
||||||
|
// ENCRYPTION: AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC)
|
||||||
|
// - Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80 bytes
|
||||||
|
// - Provides confidentiality (CBC) + authenticity (HMAC)
|
||||||
|
// - Random IV for each session
|
||||||
|
// - HMAC prevents tampering and padding oracle attacks
|
||||||
|
//
|
||||||
|
// PRODUCTION CHECKLIST:
|
||||||
|
// - Remove console.log statements that expose secrets (marked with ⚠️)
|
||||||
|
// - Ensure input field has autocomplete="off" (already set)
|
||||||
|
// - Consider adding Content-Security-Policy headers
|
||||||
|
// - Use subresource integrity (SRI) for CDN libraries in production
|
||||||
|
// ===============================================================================
|
||||||
|
|
||||||
|
let deviceIdentity = null;
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Secure memory wiping functions
|
||||||
|
function secureWipeArray(arr) {
|
||||||
|
if (!arr) return;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function secureWipeWordArray(wordArray) {
|
||||||
|
if (!wordArray || !wordArray.words) return;
|
||||||
|
for (let i = 0; i < wordArray.words.length; i++) {
|
||||||
|
wordArray.words[i] = 0;
|
||||||
|
}
|
||||||
|
wordArray.sigBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function secureWipeString(str) {
|
||||||
|
// Note: JavaScript strings are immutable, but we can at least dereference
|
||||||
|
// Best practice: clear the input field immediately after reading
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordArrayToByteArray(wordArray) {
|
||||||
|
const words = wordArray.words;
|
||||||
|
const sigBytes = wordArray.sigBytes;
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < sigBytes; i++) {
|
||||||
|
bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function byteArrayToWordArray(bytes) {
|
||||||
|
const words = [];
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8);
|
||||||
|
}
|
||||||
|
return CryptoJS.lib.WordArray.create(words, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDeviceIdentity() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/identity');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch device identity');
|
||||||
|
deviceIdentity = await response.json();
|
||||||
|
|
||||||
|
console.log('Device Public Key loaded');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading device identity:', error);
|
||||||
|
showStatus('Failed to load device identity', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performSecureUnlock() {
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
showStatus('Please enter a password', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('unlockBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
showStatus('Initializing secure connection...', 'info', true);
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Track all sensitive variables for cleanup
|
||||||
|
let sessionKeyHash = null;
|
||||||
|
let sessionKeyBytes = null;
|
||||||
|
let sharedSecretBytes = null;
|
||||||
|
let aesKeyHash = null;
|
||||||
|
let aesKeyBytes = null;
|
||||||
|
let clientKey = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load device identity if not already loaded
|
||||||
|
if (!deviceIdentity) {
|
||||||
|
const loaded = await loadDeviceIdentity();
|
||||||
|
if (!loaded) {
|
||||||
|
btn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('Computing zero-knowledge proof...', 'info', true);
|
||||||
|
|
||||||
|
// Step 1: Derive session key using PBKDF2
|
||||||
|
// PBKDF2-HMAC-SHA256 with 10000 iterations
|
||||||
|
// Use MAC address as salt (received from device identity)
|
||||||
|
const macBytes = hexToBytes(deviceIdentity.macAddress);
|
||||||
|
const salt = byteArrayToWordArray(macBytes);
|
||||||
|
sessionKeyHash = CryptoJS.PBKDF2(password, salt, {
|
||||||
|
keySize: 256/32, // 256 bits = 8 words
|
||||||
|
iterations: 10000,
|
||||||
|
hasher: CryptoJS.algo.SHA256
|
||||||
|
});
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear password from input field immediately
|
||||||
|
passwordInput.value = '';
|
||||||
|
|
||||||
|
sessionKeyBytes = wordArrayToByteArray(sessionKeyHash);
|
||||||
|
const sessionKeyHex = bytesToHex(sessionKeyBytes);
|
||||||
|
|
||||||
|
// ⚠️ PRODUCTION WARNING: Remove console.log statements in production builds
|
||||||
|
// These logs expose sensitive cryptographic material
|
||||||
|
console.log('Salt (MAC Address):', deviceIdentity.macAddress);
|
||||||
|
console.log('Session Key (PBKDF2):', sessionKeyHex);
|
||||||
|
|
||||||
|
showStatus('Establishing ECIES tunnel...', 'info', true);
|
||||||
|
|
||||||
|
// Step 2: Generate ephemeral client keypair using elliptic
|
||||||
|
const ec = new elliptic.ec('p256');
|
||||||
|
clientKey = ec.genKeyPair();
|
||||||
|
|
||||||
|
// Export uncompressed public key (0x04 + X + Y)
|
||||||
|
const clientPubHex = clientKey.getPublic('hex');
|
||||||
|
console.log('Client Public Key:', clientPubHex); // Public key - safe to log
|
||||||
|
|
||||||
|
// Step 3: Import server public key and derive shared secret (ECDH)
|
||||||
|
const serverKey = ec.keyFromPublic(deviceIdentity.pubKey, 'hex');
|
||||||
|
const sharedPoint = clientKey.derive(serverKey.getPublic());
|
||||||
|
|
||||||
|
// Convert BN to 32-byte array
|
||||||
|
const sharedSecretHex = sharedPoint.toString(16).padStart(64, '0');
|
||||||
|
sharedSecretBytes = hexToBytes(sharedSecretHex);
|
||||||
|
|
||||||
|
// ⚠️ PRODUCTION WARNING: Remove in production - exposes shared secret
|
||||||
|
console.log('Shared Secret:', sharedSecretHex);
|
||||||
|
|
||||||
|
// Step 4: Derive separate keys for encryption and authentication
|
||||||
|
// This prevents key reuse vulnerabilities in Encrypt-then-MAC
|
||||||
|
const sharedSecretWA = byteArrayToWordArray(sharedSecretBytes);
|
||||||
|
|
||||||
|
// Encryption key: SHA256("encryption" || shared_secret)
|
||||||
|
const encKeyHash = CryptoJS.SHA256(
|
||||||
|
CryptoJS.enc.Utf8.parse('encryption').concat(sharedSecretWA)
|
||||||
|
);
|
||||||
|
const encKeyBytes = wordArrayToByteArray(encKeyHash);
|
||||||
|
|
||||||
|
// MAC key: SHA256("authentication" || shared_secret)
|
||||||
|
const macKeyHash = CryptoJS.SHA256(
|
||||||
|
CryptoJS.enc.Utf8.parse('authentication').concat(sharedSecretWA)
|
||||||
|
);
|
||||||
|
const macKeyBytes = wordArrayToByteArray(macKeyHash);
|
||||||
|
|
||||||
|
// ⚠️ PRODUCTION WARNING: Remove in production - exposes keys
|
||||||
|
console.log('Encryption Key:', bytesToHex(encKeyBytes));
|
||||||
|
console.log('MAC Key:', bytesToHex(macKeyBytes));
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear shared secret after deriving keys
|
||||||
|
secureWipeArray(sharedSecretBytes);
|
||||||
|
secureWipeWordArray(sharedSecretWA);
|
||||||
|
|
||||||
|
showStatus('Encrypting credentials...', 'info', true);
|
||||||
|
|
||||||
|
// Step 5: Encrypt the session key hash with AES-256-CBC
|
||||||
|
|
||||||
|
// Generate random IV (16 bytes for CBC)
|
||||||
|
const ivWords = CryptoJS.lib.WordArray.random(16);
|
||||||
|
|
||||||
|
// Convert encryption key bytes to WordArray
|
||||||
|
const encKey = byteArrayToWordArray(encKeyBytes);
|
||||||
|
|
||||||
|
// Encrypt with AES-256-CBC using NoPadding
|
||||||
|
// Session key hash is 32 bytes (exactly 2 blocks), so no padding needed
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(
|
||||||
|
sessionKeyHash,
|
||||||
|
encKey,
|
||||||
|
{
|
||||||
|
iv: ivWords,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.NoPadding
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear session key after encryption
|
||||||
|
secureWipeArray(sessionKeyBytes);
|
||||||
|
secureWipeWordArray(sessionKeyHash);
|
||||||
|
|
||||||
|
// Extract IV and ciphertext
|
||||||
|
const ivBytes = wordArrayToByteArray(ivWords);
|
||||||
|
const ciphertextBytes = wordArrayToByteArray(encrypted.ciphertext);
|
||||||
|
|
||||||
|
showStatus('Computing authentication tag...', 'info', true);
|
||||||
|
|
||||||
|
// Build complete blob: IV (16) + Ciphertext (32) + HMAC (32) = 80 bytes
|
||||||
|
// Note: We only include first 32 bytes of ciphertext (should be exactly 32 after padding)
|
||||||
|
const completeBlob = new Uint8Array(80);
|
||||||
|
completeBlob.set(ivBytes, 0); // IV at offset 0
|
||||||
|
completeBlob.set(ciphertextBytes.slice(0, 32), 16); // First 32 bytes of ciphertext at offset 16
|
||||||
|
|
||||||
|
// Step 6: Compute HMAC-SHA256 over IV + Ciphertext (Encrypt-then-MAC)
|
||||||
|
// CRITICAL: Compute HMAC over the EXACT data in the blob (first 48 bytes: IV + CT)
|
||||||
|
const dataToAuthenticateBytes = new Uint8Array(48);
|
||||||
|
dataToAuthenticateBytes.set(completeBlob.slice(0, 48));
|
||||||
|
const dataToAuthenticate = byteArrayToWordArray(Array.from(dataToAuthenticateBytes));
|
||||||
|
const macKey = byteArrayToWordArray(macKeyBytes);
|
||||||
|
const hmac = CryptoJS.HmacSHA256(dataToAuthenticate, macKey);
|
||||||
|
const hmacBytes = wordArrayToByteArray(hmac);
|
||||||
|
|
||||||
|
console.log('IV:', bytesToHex(ivBytes));
|
||||||
|
console.log('HMAC:', bytesToHex(hmacBytes));
|
||||||
|
|
||||||
|
// Place HMAC in blob
|
||||||
|
completeBlob.set(hmacBytes, 48); // HMAC at offset 48
|
||||||
|
|
||||||
|
const encryptedBlobHex = bytesToHex(Array.from(completeBlob));
|
||||||
|
|
||||||
|
console.log('Encrypted Blob (IV+CT+HMAC):', encryptedBlobHex); // Encrypted data - safe to log
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear all keys after encryption
|
||||||
|
secureWipeArray(encKeyBytes);
|
||||||
|
secureWipeArray(macKeyBytes);
|
||||||
|
secureWipeWordArray(encKeyHash);
|
||||||
|
secureWipeWordArray(macKeyHash);
|
||||||
|
secureWipeWordArray(encKey);
|
||||||
|
secureWipeWordArray(macKey);
|
||||||
|
secureWipeWordArray(ivWords);
|
||||||
|
secureWipeWordArray(dataToAuthenticate);
|
||||||
|
secureWipeWordArray(hmac);
|
||||||
|
|
||||||
|
showStatus('Sending unlock request...', 'info', true);
|
||||||
|
|
||||||
|
// Step 7: Send to device
|
||||||
|
const response = await fetch('/api/unlock', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
clientPub: clientPubHex,
|
||||||
|
blob: encryptedBlobHex
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Unlock request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showStatus('✅ Device unlocked successfully!', 'success');
|
||||||
|
|
||||||
|
// Reload page after 1.5s - server will show status page
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showStatus('❌ Unlock failed: ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showStatus('❌ Error: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
// ⚠️ SECURITY: Always clear sensitive data, even on error
|
||||||
|
btn.disabled = false;
|
||||||
|
passwordInput.value = ''; // Ensure password field is cleared
|
||||||
|
|
||||||
|
// Wipe all sensitive variables
|
||||||
|
if (sessionKeyHash) secureWipeWordArray(sessionKeyHash);
|
||||||
|
if (sessionKeyBytes) secureWipeArray(sessionKeyBytes);
|
||||||
|
if (sharedSecretBytes) secureWipeArray(sharedSecretBytes);
|
||||||
|
|
||||||
|
// Clear client private key if possible
|
||||||
|
if (clientKey && clientKey.priv) {
|
||||||
|
// Elliptic.js uses BN.js for private keys - zero it out
|
||||||
|
if (clientKey.priv.words) {
|
||||||
|
for (let i = 0; i < clientKey.priv.words.length; i++) {
|
||||||
|
clientKey.priv.words[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(message, type, showLoader = false) {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
status.className = 'status-' + type;
|
||||||
|
status.style.display = 'block';
|
||||||
|
|
||||||
|
if (showLoader) {
|
||||||
|
status.innerHTML = '<div class="loader"></div>' + message;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lockDevice() {
|
||||||
|
await fetch('/api/lock', { method: 'POST' });
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(milliseconds) {
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m ${seconds % 60}s`;
|
||||||
|
} else {
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On load: ask server which view to show
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/status');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.unlocked) {
|
||||||
|
document.getElementById('authTime').textContent = new Date().toLocaleString();
|
||||||
|
if (data.uptime !== undefined) {
|
||||||
|
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
||||||
|
}
|
||||||
|
document.getElementById('statusPage').classList.add('active');
|
||||||
|
} else {
|
||||||
|
document.getElementById('unlockPage').classList.add('active');
|
||||||
|
loadDeviceIdentity();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('unlockPage').classList.add('active');
|
||||||
|
loadDeviceIdentity();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Enter key to submit
|
||||||
|
document.getElementById('password').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSecureUnlock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear password field on page unload/navigation
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
if (passwordInput) {
|
||||||
|
passwordInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ⚠️ SECURITY: Clear password field when page is restored from bfcache
|
||||||
|
window.addEventListener('pageshow', (event) => {
|
||||||
|
if (event.persisted) {
|
||||||
|
// Page was restored from back/forward cache
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
if (passwordInput) {
|
||||||
|
passwordInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
#endif // ZK_WEB_PAGE_H
|
||||||
13
sdkconfig.defaults
Normal file
13
sdkconfig.defaults
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user