Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64fbd5c874 | ||
|
|
ca18e1e42d | ||
|
|
b3421c3e40 | ||
|
|
96ab9741ef | ||
|
|
2af12868e2 | ||
|
|
a0a5987ffa | ||
|
|
0b0ec5eb1a | ||
|
|
0355744103 | ||
|
|
db274ce487 | ||
|
|
56f37e092d | ||
|
|
a017dc40e0 | ||
|
|
05c055503d | ||
|
|
a7c6de2dcd | ||
|
|
16a6da817c | ||
|
|
1cf541b02d | ||
|
|
8015742e29 | ||
|
|
09f3ec2f7c | ||
|
|
5744b83288 | ||
|
|
21892c108e | ||
|
|
344add841c | ||
|
|
3e3013dde1 | ||
|
|
11d3760d7b | ||
|
|
c5f1a70658 | ||
|
|
6fd7b8ce1f | ||
|
|
1b5af2fd33 | ||
|
|
9b47883330 | ||
|
|
9a8657f663 | ||
|
|
478c3a569e | ||
|
|
10208e5fac | ||
|
|
922a45ce3a | ||
|
|
d28f691aae | ||
|
|
2e8ce777d8 | ||
|
|
8ebdb50789 | ||
|
|
a39baed82b | ||
|
|
d15eebb80f | ||
|
|
fd622dfd60 | ||
|
|
c847179ba4 | ||
|
|
69c46ab2a7 | ||
|
|
6fd09f521a | ||
|
|
430e391347 | ||
|
|
bd23b674d6 | ||
|
|
e4fa743654 | ||
|
|
44372c0108 | ||
|
|
5c214f3614 | ||
|
|
f4413b7969 | ||
|
|
cca1254740 | ||
|
|
5bb59c1422 | ||
|
|
cc5638b6e7 | ||
|
|
cc797a16df | ||
|
|
17a1b3f020 | ||
|
|
659aeb934a | ||
|
|
148259040c | ||
|
|
7ffba2b678 | ||
|
|
cfaa8ff637 | ||
|
|
1317de5cbe | ||
|
|
fac0ce6503 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,7 @@ build/
|
||||
resources/
|
||||
.test_mnemonic
|
||||
Trash/
|
||||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Brief description of what this command does"
|
||||
description: "This command increments the version number and then adds, commits and pushes to the repo."
|
||||
---
|
||||
|
||||
Run increment_and_push.sh, and supply a good git commit message. For example:
|
||||
|
||||
7
.roo/commands/push_release.md
Normal file
7
.roo/commands/push_release.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: "This increments our git tag and version number, along with compiling and uploading a new release to our gitea page."
|
||||
---
|
||||
|
||||
Run increment_and_push.sh -r -p, and supply a good git commit message. For example:
|
||||
|
||||
./increment_and_push.sh "Fixed the bug with nip05 implementation"
|
||||
13
.test_mnemonic_otp_roundtrip.tmp
Normal file
13
.test_mnemonic_otp_roundtrip.tmp
Normal file
@@ -0,0 +1,13 @@
|
||||
alarm impact educate burden vague honey horn buyer sight vocal age render
|
||||
|
||||
index 0
|
||||
|
||||
{
|
||||
"index": 0,
|
||||
"nsec": "nsec1z2lrfamae2dzax7dmnlhv497uuxe4mw0m3w694upx5x54q6dgttqvzrrwl",
|
||||
"npub": "npub1j7d7yf47w8k2kseknqjr3045jvm00u0wnt3433kk6vu67d2zamcs8ynuw4",
|
||||
"npubHex": "979be226be71ecab4336982438beb49336f7f1ee9ae358c6d6d339af3542eef1",
|
||||
"nsecHex": "12be34f77dca9a2e9bcddcff7654bee70d9aedcfdc5da2d781350d4a834d42d6",
|
||||
"fipsIpv6": "fd55:b7c6:536e:26ee:a79:6e25:6f05:a85f",
|
||||
"strDerivationPath": "m/44'/1237'/0'/0/0"
|
||||
}
|
||||
@@ -55,6 +55,9 @@ RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev
|
||||
|
||||
# Copy source files
|
||||
COPY src/ /build/src/
|
||||
COPY libotppad/ /build/libotppad/
|
||||
COPY resources/tui_continuous/ /build/resources/tui_continuous/
|
||||
COPY resources/pqclean/ /build/resources/pqclean/
|
||||
|
||||
# Build nsigner as a fully static binary
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
@@ -68,6 +71,13 @@ RUN ARCH="$(uname -m)"; \
|
||||
-I/build/nostr_core_lib \
|
||||
-I/build/nostr_core_lib/nostr_core \
|
||||
-I/build/nostr_core_lib/cjson \
|
||||
-I/build/resources/tui_continuous \
|
||||
-I/build/resources/pqclean \
|
||||
-I/build/resources/pqclean/common \
|
||||
-I/build/resources/pqclean/crypto_sign/ml-dsa-65 \
|
||||
-I/build/resources/pqclean/crypto_sign/slh-dsa-128s \
|
||||
-I/build/resources/pqclean/crypto_kem/ml-kem-768 \
|
||||
-I/build/libotppad \
|
||||
/build/src/main.c \
|
||||
/build/src/secure_mem.c \
|
||||
/build/src/mnemonic.c \
|
||||
@@ -80,6 +90,35 @@ RUN ARCH="$(uname -m)"; \
|
||||
/build/src/transport_frame.c \
|
||||
/build/src/key_store.c \
|
||||
/build/src/socket_name.c \
|
||||
/build/src/auth_envelope.c \
|
||||
/build/src/miner.c \
|
||||
/build/src/pq_crypto.c \
|
||||
/build/src/pq_drbg.c \
|
||||
/build/src/otp_pad.c \
|
||||
/build/src/http_listener.c \
|
||||
/build/libotppad/libotppad.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/sign.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/poly.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/ntt.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/sign.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/hash.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/thash.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/utils.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/wots.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/fors.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/address.c \
|
||||
/build/resources/pqclean/common/fips202.c \
|
||||
/build/resources/pqclean/common/sha2.c \
|
||||
/build/resources/pqclean/common/crypto_backend_openssl.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/reduce.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/ntt.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/cbd.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/verify.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/symmetric.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/poly.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/indcpa.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/kem.c \
|
||||
/build/resources/tui_continuous/tui_continuous.c \
|
||||
"$NOSTR_LIB" \
|
||||
-o /build/nsigner_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
|
||||
191
Makefile
191
Makefile
@@ -1,13 +1,40 @@
|
||||
CC := gcc
|
||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson
|
||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -DNOSTR_ENABLE_NSIGNER_CLIENT=1 -D_GNU_SOURCE -Isrc -Ilibotppad -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson -Iresources/tui_continuous -Iresources/pqclean -Iresources/pqclean/crypto_sign/ml-dsa-65 -Iresources/pqclean/crypto_sign/slh-dsa-128s -Iresources/pqclean/crypto_kem/ml-kem-768 -Iresources/pqclean/common
|
||||
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
|
||||
SRC_DIR := src
|
||||
BUILD_DIR := build
|
||||
TEST_DIR := tests
|
||||
CLIENT_DIR := client
|
||||
EXAMPLES_DIR := examples
|
||||
|
||||
TARGET_DEV := $(BUILD_DIR)/nsigner
|
||||
|
||||
# PQClean ML-DSA-65 sources (Phase 3)
|
||||
PQCLEAN_DIR := resources/pqclean
|
||||
PQCLEAN_SOURCES := \
|
||||
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/sign.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/poly.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/ntt.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/sign.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/hash.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/thash.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/utils.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/wots.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/fors.c \
|
||||
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/address.c \
|
||||
$(PQCLEAN_DIR)/common/fips202.c \
|
||||
$(PQCLEAN_DIR)/common/sha2.c \
|
||||
$(PQCLEAN_DIR)/common/crypto_backend_openssl.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/reduce.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/ntt.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/cbd.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/verify.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/symmetric.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/poly.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/indcpa.c \
|
||||
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/kem.c
|
||||
|
||||
SOURCES := \
|
||||
$(SRC_DIR)/main.c \
|
||||
$(SRC_DIR)/secure_mem.c \
|
||||
@@ -20,7 +47,16 @@ SOURCES := \
|
||||
$(SRC_DIR)/server.c \
|
||||
$(SRC_DIR)/transport_frame.c \
|
||||
$(SRC_DIR)/key_store.c \
|
||||
$(SRC_DIR)/socket_name.c
|
||||
$(SRC_DIR)/socket_name.c \
|
||||
$(SRC_DIR)/auth_envelope.c \
|
||||
$(SRC_DIR)/miner.c \
|
||||
$(SRC_DIR)/pq_crypto.c \
|
||||
$(SRC_DIR)/pq_drbg.c \
|
||||
$(SRC_DIR)/otp_pad.c \
|
||||
$(SRC_DIR)/http_listener.c \
|
||||
libotppad/libotppad.c \
|
||||
$(PQCLEAN_SOURCES) \
|
||||
resources/tui_continuous/tui_continuous.c
|
||||
|
||||
HEADERS :=
|
||||
|
||||
@@ -33,13 +69,32 @@ TEST_DISPATCHER_TARGET := $(BUILD_DIR)/test_dispatcher
|
||||
TEST_POLICY_TARGET := $(BUILD_DIR)/test_policy
|
||||
TEST_INTEGRATION_TARGET := $(BUILD_DIR)/test_integration
|
||||
TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
|
||||
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
|
||||
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
|
||||
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
||||
TEST_MINE_EVENT_TARGET := $(BUILD_DIR)/test_mine_event
|
||||
TEST_PQ_CRYPTO_TARGET := $(BUILD_DIR)/test_pq_crypto
|
||||
TEST_ED25519_X25519_TARGET := $(BUILD_DIR)/test_ed25519_x25519
|
||||
TEST_ML_DSA_65_TARGET := $(BUILD_DIR)/test_ml_dsa_65
|
||||
TEST_SLH_DSA_128S_TARGET := $(BUILD_DIR)/test_slh_dsa_128s
|
||||
TEST_ML_KEM_768_TARGET := $(BUILD_DIR)/test_ml_kem_768
|
||||
TEST_PUBKEY_FORMAT_TARGET := $(BUILD_DIR)/test_pubkey_format
|
||||
TEST_ALGORITHM_API_TARGET := $(BUILD_DIR)/test_algorithm_api
|
||||
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
||||
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
||||
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
||||
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
|
||||
EXAMPLE_PQ_SIGN_TARGET := $(BUILD_DIR)/example_pq_sign
|
||||
EXAMPLE_PQ_KEM_TARGET := $(BUILD_DIR)/example_pq_kem
|
||||
EXAMPLE_SSH_SIGN_TARGET := $(BUILD_DIR)/example_ssh_sign
|
||||
DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99
|
||||
|
||||
.PHONY: all lib dev static static-debug static-arm64 test test-integration test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name clean
|
||||
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-algorithm-api examples test-client clean
|
||||
|
||||
all: dev
|
||||
|
||||
lib:
|
||||
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
|
||||
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,13,19,44
|
||||
|
||||
dev: lib $(TARGET_DEV)
|
||||
|
||||
@@ -59,7 +114,10 @@ static-arm64:
|
||||
chmod +x ./build_static.sh
|
||||
./build_static.sh --arch arm64
|
||||
|
||||
test: lib test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name
|
||||
firmware-feather:
|
||||
cd firmware/feather_s3_tft && idf.py build
|
||||
|
||||
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-client
|
||||
|
||||
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_INTEGRATION_TARGET)
|
||||
@@ -67,6 +125,9 @@ test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||
test-mnemonic: $(TEST_MNEMONIC_TARGET)
|
||||
./$(TEST_MNEMONIC_TARGET)
|
||||
|
||||
test-mnemonic-input: $(TEST_MNEMONIC_INPUT_TARGET)
|
||||
./$(TEST_MNEMONIC_INPUT_TARGET)
|
||||
|
||||
test-role: $(TEST_ROLE_TARGET)
|
||||
./$(TEST_ROLE_TARGET)
|
||||
|
||||
@@ -85,9 +146,47 @@ test-policy: $(TEST_POLICY_TARGET)
|
||||
test-socket-name: $(TEST_SOCKET_NAME_TARGET)
|
||||
./$(TEST_SOCKET_NAME_TARGET)
|
||||
|
||||
test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
|
||||
./$(TEST_AUTH_ENVELOPE_TARGET)
|
||||
|
||||
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_QREXEC_AUTH_TARGET)
|
||||
|
||||
test-mine-event: $(TEST_MINE_EVENT_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_MINE_EVENT_TARGET)
|
||||
|
||||
test-pq-crypto: $(TEST_PQ_CRYPTO_TARGET)
|
||||
./$(TEST_PQ_CRYPTO_TARGET)
|
||||
|
||||
test-ed25519-x25519: $(TEST_ED25519_X25519_TARGET)
|
||||
./$(TEST_ED25519_X25519_TARGET)
|
||||
|
||||
test-ml-dsa-65: $(TEST_ML_DSA_65_TARGET)
|
||||
./$(TEST_ML_DSA_65_TARGET)
|
||||
|
||||
test-slh-dsa-128s: $(TEST_SLH_DSA_128S_TARGET)
|
||||
./$(TEST_SLH_DSA_128S_TARGET)
|
||||
|
||||
test-ml-kem-768: $(TEST_ML_KEM_768_TARGET)
|
||||
./$(TEST_ML_KEM_768_TARGET)
|
||||
|
||||
test-pubkey-format: $(TEST_PUBKEY_FORMAT_TARGET)
|
||||
./$(TEST_PUBKEY_FORMAT_TARGET)
|
||||
|
||||
test-algorithm-api: $(TEST_ALGORITHM_API_TARGET)
|
||||
./$(TEST_ALGORITHM_API_TARGET)
|
||||
|
||||
test-client: examples
|
||||
|
||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(EXAMPLE_PQ_SIGN_TARGET) $(EXAMPLE_PQ_KEM_TARGET) $(EXAMPLE_SSH_SIGN_TARGET) $(DEMO_C99_TARGET)
|
||||
|
||||
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(TEST_DIR)/test_mnemonic_input.c -o $(TEST_MNEMONIC_INPUT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ROLE_TARGET): $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -97,13 +196,13 @@ $(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c -o $(TEST_SELECTOR_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c
|
||||
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -117,5 +216,77 @@ $(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_nam
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c -o $(TEST_SOCKET_NAME_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_AUTH_ENVELOPE_TARGET): $(TEST_DIR)/test_auth_envelope.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_auth_envelope.c $(SRC_DIR)/auth_envelope.c -o $(TEST_AUTH_ENVELOPE_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_MINE_EVENT_TARGET): $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c -o $(TEST_PQ_CRYPTO_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ED25519_X25519_TARGET): $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ML_DSA_65_TARGET): $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_SLH_DSA_128S_TARGET): $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ML_KEM_768_TARGET): $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_PUBKEY_FORMAT_TARGET): $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ALGORITHM_API_TARGET): $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ALGORITHM_API_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_public_key_client.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_SIGN_EVENT_TARGET): $(EXAMPLES_DIR)/sign_event_client.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/sign_event_client.c -o $(EXAMPLE_SIGN_EVENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBKEY_TCP_TARGET): $(EXAMPLES_DIR)/get_pubkey_tcp.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_tcp.c -o $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBKEY_QREXEC_TARGET): $(EXAMPLES_DIR)/get_pubkey_qrexec.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_qrexec.c -o $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_PQ_SIGN_TARGET): $(EXAMPLES_DIR)/pq_sign_example.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/pq_sign_example.c -o $(EXAMPLE_PQ_SIGN_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_PQ_KEM_TARGET): $(EXAMPLES_DIR)/pq_kem_example.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/pq_kem_example.c -o $(EXAMPLE_PQ_KEM_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_SSH_SIGN_TARGET): $(EXAMPLES_DIR)/ssh_sign_example.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/ssh_sign_example.c -o $(EXAMPLE_SSH_SIGN_TARGET) $(LDFLAGS)
|
||||
|
||||
$(DEMO_C99_TARGET): $(CLIENT_DIR)/demo_c99.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(CLIENT_DIR)/demo_c99.c -o $(DEMO_C99_TARGET) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
703
README.md
703
README.md
@@ -20,7 +20,8 @@ This is a **program, not a daemon**:
|
||||
- purpose/curve enforcement
|
||||
- request dispatch
|
||||
- interactive terminal UI
|
||||
- transport adapter(s)
|
||||
- transport adapter(s) (Unix socket, qrexec, FIPS/TCP, HTTP)
|
||||
- OTP one-time pad encryption (optional, with USB pad)
|
||||
|
||||
You run it when you need signing. You stop it when you are done. Closing the terminal or quitting the program ends the trust session and destroys state.
|
||||
|
||||
@@ -28,42 +29,28 @@ You run it when you need signing. You stop it when you are done. Closing the ter
|
||||
|
||||
### 2.1 Zero filesystem footprint
|
||||
|
||||
At runtime, `n_signer` writes nothing to disk:
|
||||
|
||||
- no config files
|
||||
- no logs
|
||||
- no PID files
|
||||
- no lock files
|
||||
- no socket pathname artifacts
|
||||
|
||||
On Linux desktop, local IPC uses abstract namespace Unix sockets (`@name` semantics) that exist only in kernel memory and disappear with process/kernel namespace lifetime.
|
||||
At runtime, `n_signer` writes nothing to disk: no config files, no logs, no PID files, no lock files, no socket pathname artifacts. On Linux desktop, local IPC uses abstract namespace Unix sockets (`@name` semantics) that exist only in kernel memory and disappear with process/kernel namespace lifetime.
|
||||
|
||||
### 2.2 Crash = total wipe
|
||||
|
||||
All sensitive and operational state exists only in-process RAM (mlock'd where applicable):
|
||||
|
||||
- mnemonic-derived key material
|
||||
- role table
|
||||
- policy/approval decisions for the live session
|
||||
- activity display buffer
|
||||
|
||||
If the process dies (fault, kill, exploit, power loss), state is unrecoverable by design. There is no persistence layer to scrape post-crash.
|
||||
All sensitive and operational state exists only in-process RAM (mlock'd where applicable): mnemonic-derived key material, role table, policy/approval decisions for the live session, activity display buffer. If the process dies (fault, kill, exploit, power loss), state is unrecoverable by design.
|
||||
|
||||
### 2.3 Single binary, no external dependencies
|
||||
|
||||
Runtime target is one statically-linked musl executable:
|
||||
|
||||
- no shared libraries required at runtime
|
||||
- no interpreter/runtime VM dependency
|
||||
- no sidecar services
|
||||
- no helper daemon binaries
|
||||
|
||||
This reduces deployment variability and shrinks the runtime trust surface.
|
||||
Runtime target is one statically-linked musl executable: no shared libraries required at runtime, no interpreter/runtime VM dependency, no sidecar services, no helper daemon binaries. This reduces deployment variability and shrinks the runtime trust surface.
|
||||
|
||||
### 2.4 Always-attended operation
|
||||
|
||||
`n_signer` is intentionally human-attended. It stays attached to a terminal and can require explicit keystroke approval for unknown callers or sensitive actions. Human presence is part of the security model.
|
||||
|
||||
### 2.5 Secret memory backing: `mlock` today, `memfd_secret` where supported
|
||||
|
||||
Sensitive buffers (mnemonic, master seed, per-role private keys) live in `mlock`'d RAM via [`secure_buf_alloc`](src/secure_mem.c) and are zeroized with `secure_memzero` on free. This gives swap protection and crash-wipe semantics on every supported platform, including Qubes OS Xen guests.
|
||||
|
||||
`memfd_secret(2)` (Linux `CONFIG_SECRETMEM`) is a stronger backing: pages are invisible to `/proc/pid/mem`, `ptrace`, `kcore`, and hibernation dumps, and are kernel-guaranteed to be wiped on exit. It is the intended tier-1 upgrade for the `secure_buf_alloc` path on hosts that can materialize secretmem pages — bare metal and KVM guests.
|
||||
|
||||
It is **not** used yet because Qubes OS VMs are Xen guests: the `memfd_secret` syscall succeeds and returns a valid `/secretmem` fd, but the first page-fault into the mapping raises `SIGBUS` (the Xen hypervisor cannot back the restricted pages). Since Qubes integration is a primary deployment target, the `mlock` path remains correct and universal. A future implementation will probe `memfd_secret` by writing a canary byte into a trial mapping and fall back to `mlock` on `SIGBUS`/`ENOSYS`, enabling the stronger backing automatically on non-Xen hosts.
|
||||
|
||||
## 3. How it works
|
||||
|
||||
### 3.1 Startup phase (TUI input mode)
|
||||
@@ -74,56 +61,74 @@ When started, `n_signer` immediately enters terminal input mode:
|
||||
- On `E`: prompt for mnemonic with terminal echo disabled, then validate.
|
||||
- On `G`: generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning, then continue. There is no confirmation step.
|
||||
2. Build in-memory role/selector state from the mnemonic.
|
||||
3. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` override).
|
||||
4. Initialize transport endpoints and bind the socket.
|
||||
5. Switch to running status display, with the signer name and socket address shown in the banner.
|
||||
3. **Interactive transport selection** (if no `--listen` flag given and stdin is a TTY): choose one or more of: Local Unix socket, Qubes qrexec bridge, FIPS/TCP listener (framed JSON), HTTP listener (curl-friendly).
|
||||
4. **Index whitelist** (optional): restrict which `nostr_index` values this session can access.
|
||||
5. **OTP pad selection** (optional): auto-scans attached USB drives for OTP pads and offers to bind one. See [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md).
|
||||
6. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` override).
|
||||
7. Initialize transport endpoints and bind the socket.
|
||||
8. Switch to running status display.
|
||||
|
||||
No startup files are read or written. The mnemonic — typed or generated — lives only in `mlock`'d memory and is zeroized on shutdown or crash.
|
||||
|
||||
For parent-process launchers, startup can also be non-interactive:
|
||||
|
||||
- `--mnemonic-stdin`: read one mnemonic line from stdin at startup, then continue normally.
|
||||
- `--mnemonic-fd N`: read one mnemonic line from inherited file descriptor `N` at startup.
|
||||
|
||||
These modes avoid putting mnemonic material in argv/environment and are designed for supervised spawners. See [`plans/mnemonic_startup_input.md`](plans/mnemonic_startup_input.md) for the full behavior contract.
|
||||
|
||||
### 3.2 Running phase (status display + signer)
|
||||
|
||||
After unlock, terminal becomes a live status and control console. Example layout:
|
||||
After unlock, the terminal becomes a live status and control console rendered by [`render_status()`](src/main.c). The top frame shows the program name and version; below it are the Roles and Activity sections, followed by a single status line. Connection instructions are not shown by default — press `d` to display them on demand. Example layout:
|
||||
|
||||
```text
|
||||
n_signer v0.x | foreground session active
|
||||
transport: unix-abstract:@nsigner
|
||||
session: unlocked (RAM-only)
|
||||
n_signer v0.0.53 > Main Menu
|
||||
|
||||
Roles
|
||||
-----
|
||||
main purpose=nostr curve=secp256k1 selector=role:main
|
||||
ops purpose=nostr curve=secp256k1 selector=nostr_index:7
|
||||
backup purpose=bitcoin curve=secp256k1 selector=role_path:m/84'/0'/0'/0/5
|
||||
Roles:
|
||||
Role Purpose Curve Derivation path
|
||||
main nostr secp256k1 m/44'/1237'/0'/0/0
|
||||
nostr_idx_1 nostr secp256k1 m/44'/1237'/1'/0/0
|
||||
backup bitcoin secp256k1 m/84'/0'/0'/0/5
|
||||
|
||||
Pending approvals
|
||||
-----------------
|
||||
(none)
|
||||
Activity (latest first):
|
||||
16:03:11 allow caller=uid:1000 method=nostr_get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=nostr_sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=nostr_sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=nostr_sign_event error=unauthorized
|
||||
|
||||
Activity (latest first)
|
||||
-----------------------
|
||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
Hotkeys
|
||||
-------
|
||||
a toggle auto-approve(prompt) for this session
|
||||
r refresh
|
||||
l lock/reunlock session
|
||||
q quit
|
||||
l lock/reunlock
|
||||
r refresh
|
||||
a toggle auto-approve
|
||||
d display connections
|
||||
q/x quit
|
||||
```
|
||||
|
||||
The **Derivation path** column shows the full BIP-44 path for each role's key. For `nostr_index` roles this is `m/44'/1237'/<n>'/0/0` (NIP-06); for `role_path` roles it's the explicit path.
|
||||
|
||||
The signer's name (`nsigner_hairy_dog` in this example) appears in the **status line** at the bottom (`signer=nsigner_hairy_dog`). See [§4.1](#41-linux-desktop-abstract-namespace-unix-socket) for how the name is generated.
|
||||
|
||||
### Connection instructions (press `d`)
|
||||
|
||||
Pressing `d` clears the screen and shows each active transport as a titled block with the connection string and an example client command. Press any key to return to the status display.
|
||||
|
||||
Hotkeys (active while the status display is shown):
|
||||
|
||||
- `a` — toggle auto-approve (prompt) for this session
|
||||
- `r` — refresh the display
|
||||
- `d` — display connection instructions (press any key to return)
|
||||
- `l` — lock / re-unlock the session
|
||||
- `q` — quit
|
||||
|
||||
### 3.3 Approval prompts
|
||||
|
||||
When a request needs confirmation, `n_signer` interrupts the status view with a prompt and waits for a local keystroke.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Approval required
|
||||
caller: uid:1000
|
||||
method: sign_event
|
||||
method: nostr_sign_event
|
||||
selector: role=ops
|
||||
purpose/curve: nostr/secp256k1
|
||||
|
||||
@@ -138,120 +143,431 @@ No response is emitted to caller until the local user decides.
|
||||
- `l` locks the session in-place: signing stops until mnemonic is re-entered.
|
||||
- terminal close or process termination has the same effect as quit: total state wipe.
|
||||
|
||||
## 4. Mnemonic-rooted role model
|
||||
## 4. API
|
||||
|
||||
`n_signer` derives many role-scoped keys from one mnemonic root. Requests select a role using explicit selectors, then enforcement checks operation compatibility.
|
||||
`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This section is the complete, authoritative description of the API.
|
||||
|
||||
### 4.1 Nostr shorthand: nostr_index
|
||||
For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
|
||||
Nostr shorthand keeps the explicit index selector:
|
||||
### 4.1 Request format
|
||||
|
||||
`m/44'/1237'/<nostr_index>'/0/0`
|
||||
|
||||
Use `nostr_index` only for Nostr-indexed roles.
|
||||
|
||||
### 4.2 Full derivation path: role_path
|
||||
|
||||
For non-Nostr or advanced layouts, caller may use full `role_path` selector.
|
||||
|
||||
Security rule: `role_path` must match a pre-registered role entry. Unregistered ad-hoc derivation requests are rejected.
|
||||
|
||||
### 4.3 What memorizing your seed phrase gets you
|
||||
|
||||
A single memorized mnemonic can deterministically recover multiple key domains through role definitions, not just one identity.
|
||||
|
||||
Examples include Nostr roles, Bitcoin branches, and future application-specific paths. See [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md) for the maintained use-case catalog and caveats.
|
||||
|
||||
## 5. Wire contract (JSON-RPC)
|
||||
|
||||
Request shape is JSON-RPC with NIP-46-style methods and optional trailing selector options.
|
||||
|
||||
```jsonc
|
||||
{ "id": "1", "method": "get_public_key", "params": [] }
|
||||
{ "id": "2", "method": "sign_event", "params": ["<event_json>", { "role": "main" }] }
|
||||
{ "id": "3", "method": "sign_event", "params": ["<event_json>", { "nostr_index": 7 }] }
|
||||
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
|
||||
```json
|
||||
{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] }
|
||||
```
|
||||
|
||||
Implemented signer verbs in this build:
|
||||
- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses.
|
||||
- `method` — the verb name (see [§4.2](#42-verbs)).
|
||||
- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs.
|
||||
|
||||
- `get_public_key`
|
||||
- `sign_event`
|
||||
- `nip04_encrypt` / `nip04_decrypt`
|
||||
- `nip44_encrypt` / `nip44_decrypt`
|
||||
### 4.2 Response format
|
||||
|
||||
Selector resolution order:
|
||||
Success:
|
||||
```json
|
||||
{ "id": "<string>", "result": <value> }
|
||||
```
|
||||
|
||||
1. `role`
|
||||
2. `nostr_index`
|
||||
3. `role_path`
|
||||
4. default role `main`
|
||||
`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it.
|
||||
|
||||
Conflicting selectors are rejected (`ambiguous_role_selector`).
|
||||
Error:
|
||||
```json
|
||||
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
Representative error codes:
|
||||
Error codes:
|
||||
|
||||
- `invalid_request`
|
||||
- `method_not_found`
|
||||
- `ambiguous_role_selector`
|
||||
- `unknown_role`
|
||||
- `purpose_mismatch`
|
||||
- `curve_mismatch`
|
||||
- `unauthorized`
|
||||
- `approval_denied`
|
||||
- `internal_error`
|
||||
| Code | Message | Meaning |
|
||||
|-------|-------------------------------|--------------------------------------------------------------------|
|
||||
| -32700| `parse_error` | Request was not valid JSON. |
|
||||
| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. |
|
||||
| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. |
|
||||
| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). |
|
||||
| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. |
|
||||
| 1002 | `unknown_role` | No role matched the selector. |
|
||||
| 1003 | `no_default_role` | No selector given and no `main` role exists. |
|
||||
| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. |
|
||||
| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. |
|
||||
| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. |
|
||||
| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. |
|
||||
| 1008 | `mining_failed` | Internal error during proof-of-work mining. |
|
||||
| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. |
|
||||
| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. |
|
||||
|
||||
## 6. Purpose and curve enforcement
|
||||
### 4.3 Verbs
|
||||
|
||||
Selector resolution chooses *which* role. Enforcement decides *whether the requested method is valid* for that role.
|
||||
All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§4.4](#44-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto.
|
||||
|
||||
Example:
|
||||
| Verb | Algorithms | Positional params | Options |
|
||||
|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------|
|
||||
| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>`, `<signature_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `encapsulate` | ml-kem-768 | `<peer_pubkey_hex>` | `algorithm` |
|
||||
| `decapsulate` | ml-kem-768 | `<ciphertext_hex>` | `algorithm`, `index` |
|
||||
| `derive_shared_secret` | x25519 | `<peer_pubkey_hex>` | `algorithm`, `index` |
|
||||
| `derive` | secp256k1 | `<data>` | `algorithm`, `index` (required) |
|
||||
| `encrypt` | otp | `<plaintext_base64>` | `algorithm`, `encoding` |
|
||||
| `decrypt` | otp | `<ciphertext>` | `algorithm`, `encoding` |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index` |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` |
|
||||
| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
|
||||
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
|
||||
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
|
||||
\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
|
||||
|
||||
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
|
||||
#### Enforcement matrix
|
||||
|
||||
## 7. Transport
|
||||
| Verb | Valid algorithms |
|
||||
|----------------------------|-----------------------------------------------|
|
||||
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `get_public_key` | all key-deriving algorithms |
|
||||
| `nostr_*` | secp256k1 (Nostr protocol) |
|
||||
|
||||
### 7.1 Linux desktop: abstract namespace Unix socket
|
||||
Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010).
|
||||
|
||||
Primary local transport is AF_UNIX abstract namespace.
|
||||
### 4.4 Algorithms
|
||||
|
||||
Each running `nsigner` process binds to a unique abstract name of the form `@nsigner_<word1>_<word2>`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host.
|
||||
All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§4.4.3](#443-otp)).
|
||||
|
||||
Properties:
|
||||
#### 4.4.1 Algorithm table
|
||||
|
||||
- no pathname in filesystem
|
||||
- endpoint lifetime bound to process/kernel namespace
|
||||
- no stale socket files
|
||||
- caller identity via peer credentials (`SO_PEERCRED`)
|
||||
- per-launch random name avoids collisions between concurrent instances and leaks no seed-derived identifier
|
||||
| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) |
|
||||
|-----------------|-----------------|---------------|---------------------------------------|-------------------------------|
|
||||
| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 |
|
||||
| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 |
|
||||
| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 |
|
||||
| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 |
|
||||
| `otp` | One-time pad | — | (no key — bound USB pad) | n/a |
|
||||
|
||||
#### 4.4.2 Key derivation
|
||||
|
||||
- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar.
|
||||
- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key.
|
||||
- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time.
|
||||
- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests.
|
||||
|
||||
The PQ implementations are vendored from [PQClean](https://github.com/PQClean/PQClean) (public domain / CC0). All six algorithms are always compiled in on every target. The three post-quantum algorithms address the **harvest-now-decrypt-later** threat: an adversary recording encrypted traffic today to decrypt it once a quantum computer becomes available.
|
||||
|
||||
#### 4.4.3 OTP
|
||||
|
||||
The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response.
|
||||
|
||||
### 4.5 Examples
|
||||
|
||||
#### `get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
`key_id` is the first 16 hex characters of the public key — a short display identifier.
|
||||
|
||||
#### `sign`
|
||||
|
||||
```json
|
||||
{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`:
|
||||
|
||||
```json
|
||||
{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] }
|
||||
```
|
||||
|
||||
#### `verify`
|
||||
|
||||
```json
|
||||
{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" }
|
||||
```
|
||||
|
||||
The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library.
|
||||
|
||||
#### `encapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`.
|
||||
|
||||
#### `decapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
#### `derive_shared_secret` (X25519)
|
||||
|
||||
```json
|
||||
{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305).
|
||||
|
||||
#### `derive` (secp256k1 HMAC-SHA256)
|
||||
|
||||
```json
|
||||
{ "id": "10", "method": "derive", "params": [ "<data>", { "algorithm": "secp256k1", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "10", "result": "{\"algorithm\":\"secp256k1\",\"key_id\":\"<16hex>\",\"digest\":\"<64hex>\"}" }
|
||||
```
|
||||
|
||||
Computes `HMAC-SHA256(privkey, data)` where `privkey` is the secp256k1 private key derived on demand at `(algorithm: "secp256k1", index: N)`. `data` is an arbitrary caller-supplied UTF-8 string. Returns the 32-byte digest as 64 lowercase hex chars.
|
||||
|
||||
`index` is **required** (no default) — forces conscious selection of which derived key to use as the HMAC key. Omitting it returns `missing_index`.
|
||||
|
||||
This is a generic key-derived MAC primitive. Callers domain-separate by prefixing their own label into `data` (e.g. `"myapp/identifier-v1:<path>"`). The private key never leaves the signer; only the digest is returned. Use cases include deterministic, per-user, opaque identifiers for NIP-33 parameterized-replaceable events (e.g. bookmark folder `d` tags) where the same logical name must produce the same `d` tag across devices.
|
||||
|
||||
#### `encrypt` / `decrypt` (OTP)
|
||||
|
||||
```json
|
||||
{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
```
|
||||
|
||||
`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used.
|
||||
|
||||
`encrypt` response:
|
||||
```json
|
||||
{
|
||||
"id": "8",
|
||||
"result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}"
|
||||
}
|
||||
```
|
||||
|
||||
`decrypt` response:
|
||||
```json
|
||||
{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" }
|
||||
```
|
||||
|
||||
If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`.
|
||||
|
||||
#### `nostr_get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
Response (default): a plain 64-hex-char secp256k1 public key string.
|
||||
Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
|
||||
|
||||
#### `nostr_sign_event`
|
||||
|
||||
Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event.
|
||||
|
||||
```json
|
||||
{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`<event_json>` is the unsigned event object:
|
||||
```json
|
||||
{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" }
|
||||
```
|
||||
|
||||
Response: the signed event JSON string, with `id` and `sig` populated.
|
||||
|
||||
#### `nostr_mine_event`
|
||||
|
||||
Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"method": "nostr_mine_event",
|
||||
"params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ]
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Required | Default | Meaning |
|
||||
|---------------|-------------------------------|---------|---------------------------------------------------------------|
|
||||
| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. |
|
||||
| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. |
|
||||
| `threads` | no | 1 | Mining threads (clamped to 1..32). |
|
||||
|
||||
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}"
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given.
|
||||
- `1008` `mining_failed` — internal mining error.
|
||||
|
||||
#### `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
|
||||
|
||||
NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload.
|
||||
|
||||
```json
|
||||
{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
#### `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
|
||||
|
||||
NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format.
|
||||
|
||||
```json
|
||||
{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
### 4.6 Role-based selectors (Nostr verbs)
|
||||
|
||||
The `nostr_*` verbs select a secp256k1 NIP-06 key via the options object. Supported selectors:
|
||||
|
||||
| Selector | Meaning |
|
||||
|----------------|--------------------------------------------------|
|
||||
| `nostr_index` | NIP-06 index `n` → path `m/44'/1237'/<n>'/0/0` |
|
||||
| `role` | Name of a pre-registered role entry |
|
||||
| `role_path` | Full BIP-44 derivation path (must match a registered role) |
|
||||
|
||||
Selector resolution order: `role` → `nostr_index` → `role_path` → default role `main`. Conflicting selectors are rejected with `ambiguous_role_selector` (1001). The role's `(purpose, curve)` must be `(nostr, secp256k1)` — any other combination is rejected with `purpose_mismatch` (1004) or `curve_mismatch` (1005).
|
||||
|
||||
### 4.7 Pre-approval
|
||||
|
||||
Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`.
|
||||
|
||||
Algorithm-based:
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate
|
||||
```
|
||||
|
||||
Nostr (role-based):
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key
|
||||
```
|
||||
|
||||
A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others).
|
||||
|
||||
## 5. Transports
|
||||
|
||||
The API is transport-independent. The same JSON request works over every transport; only the framing differs.
|
||||
|
||||
| Transport | `--listen` flag | Framing | Caller identity |
|
||||
|-----------|--------------------------------|------------------------------------------|----------------------------|
|
||||
| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED` → `uid:<n>` |
|
||||
| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid |
|
||||
| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` |
|
||||
| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) |
|
||||
| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) |
|
||||
|
||||
### 5.1 HTTP examples
|
||||
|
||||
Start the signer:
|
||||
```bash
|
||||
nsigner --listen http:127.0.0.1:11111 --allow-all
|
||||
```
|
||||
|
||||
Get a public key:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
Sign a Nostr event:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
```
|
||||
|
||||
OTP encrypt:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}'
|
||||
```
|
||||
|
||||
### 5.2 Unix socket examples (framed mode)
|
||||
|
||||
```bash
|
||||
# get_public_key
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
|
||||
# Sign a Nostr event
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
|
||||
# ed25519 sign
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
### 5.3 Linux desktop: abstract namespace Unix socket
|
||||
|
||||
Primary local transport is AF_UNIX abstract namespace. Each running `nsigner` process binds to a unique abstract name of the form `@nsigner_<word1>_<word2>`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host.
|
||||
|
||||
Properties: no pathname in filesystem; endpoint lifetime bound to process/kernel namespace; no stale socket files; caller identity via peer credentials (`SO_PEERCRED`); per-launch random name avoids collisions and leaks no seed-derived identifier.
|
||||
|
||||
Naming rules:
|
||||
|
||||
- Default: random pick at startup, displayed in the TUI banner.
|
||||
- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name (useful for scripts and tests).
|
||||
- Collision: if the chosen random name is already bound by another process, `nsigner` retries with a fresh pair up to 8 times before erroring out with a hint to use an explicit name override.
|
||||
- Default: random pick at startup, displayed in the status line.
|
||||
- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name.
|
||||
- Collision: if the chosen random name is already bound, `nsigner` retries with a fresh pair up to 8 times before erroring out.
|
||||
|
||||
Discovery:
|
||||
|
||||
- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`.
|
||||
- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout.
|
||||
- `nsigner --listen qrexec` is the same stdio framing mode, but caller identity can be derived from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`).
|
||||
- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:8080`, `tcp:[::]:8080`, or `tcp:[fd00::1234]:8080`).
|
||||
- `nsigner --listen qrexec` is the same stdio framing, but caller identity comes from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`).
|
||||
- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables FIPS/TCP listening (framed JSON, not HTTP).
|
||||
- `nsigner --listen http:HOST:PORT` enables HTTP listening for curl-friendly access. CORS headers included for browser access. Defaults to localhost; pass `http:0.0.0.0:PORT` to expose externally.
|
||||
- `nsigner bridge --to <socket-name>` is a stateless relay for Qubes qrexec: reads one framed request from stdin, forwards it to a persistent signer's abstract unix socket, and relays the response to stdout. Used as the `qubes.NsignerRpc` service entrypoint. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md).
|
||||
- `--bridge-source-trusted` (unix listener only): marks the socket as a trusted bridge endpoint. Each connection sends a framed `{"qrexec_source":"<vm>"}` preamble before the request, and the caller identity is composed as `qubes:<vm>`.
|
||||
|
||||
### 7.2 ESP32 MCU: USB-CDC serial
|
||||
### 5.4 ESP32 MCU: TinyUSB composite (CDC + WebUSB)
|
||||
|
||||
On MCU targets, the same core modules run with a different transport adapter: USB-CDC serial framing instead of AF_UNIX.
|
||||
On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport: CDC-ACM framed JSON-RPC for serial tooling (`/dev/ttyACM*`) and Vendor/WebUSB framed JSON-RPC for browser tooling. Core signer logic stays the same; only transport/UI bindings change.
|
||||
|
||||
Core logic stays the same; only transport/UI bindings change.
|
||||
|
||||
### 7.3 NIP-46 relay flow (both platforms)
|
||||
|
||||
NIP-46 request/response flow can be bridged on both desktop and MCU transports. The JSON-RPC signer contract remains consistent while the outer carrier differs.
|
||||
|
||||
### 7.4 Caller verification
|
||||
### 5.5 Caller verification
|
||||
|
||||
Every transport must provide concrete caller identity before policy evaluation.
|
||||
|
||||
@@ -261,134 +577,149 @@ Every transport must provide concrete caller identity before policy evaluation.
|
||||
|
||||
Identity verification and interactive approval are separate layers. Passing identity checks does not bypass prompt requirements.
|
||||
|
||||
## 8. Platform targets
|
||||
## 6. Platform targets
|
||||
|
||||
### 8.1 Linux desktop (primary)
|
||||
### 6.1 Linux desktop (primary)
|
||||
|
||||
Primary deployment is a local, foreground terminal program with abstract namespace socket transport.
|
||||
|
||||
### 8.2 ESP32 / MCU
|
||||
### 6.2 ESP32 / MCU
|
||||
|
||||
MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware (for example UART/OLED/buttons or USB-CDC host interaction).
|
||||
MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware. The Feather path currently uses TinyUSB composite USB (CDC + WebUSB) plus TFT/buttons for attended approvals.
|
||||
|
||||
### 8.3 Qubes OS qube
|
||||
### 6.3 Qubes OS qube
|
||||
|
||||
Qubes deployment runs `n_signer` in a dedicated signer qube as a foreground process under explicit user session control. Transport binding is platform-specific, but lifecycle and memory-only state model are unchanged.
|
||||
Qubes deployment runs `n_signer` in a dedicated signer qube (e.g. `nostr_signer`) as a foreground process under explicit user session control. The mnemonic lives only in mlock'd RAM in that qube — a compromised agent in a caller qube cannot read it (hypervisor-enforced memory isolation).
|
||||
|
||||
## 9. Usage
|
||||
Three transport paths are supported:
|
||||
|
||||
### 9.1 Run the program
|
||||
**FIPS/TCP** — the signer listens on `tcp:[::]:11111` and FIPS carries traffic between qubes as an IPv6 mesh substrate. See [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md).
|
||||
|
||||
**HTTP** — the signer listens on `http:127.0.0.1:11111` for curl-friendly access within the same qube. No auth envelopes required (relies on localhost binding + policy/approval prompts).
|
||||
|
||||
**Qubes qrexec bridge** (recommended for no-network deployments) — a persistent signer listens on an abstract unix socket, and a stateless `nsigner bridge` relay (the `qubes.NsignerRpc` qrexec service) forwards one request per qrexec invocation. No network, no FIPS — pure intra-host IPC. Caller identity is `qubes:<source-vm>`. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md) for the full design.
|
||||
|
||||
#### Qrexec bridge setup
|
||||
|
||||
**In the signer qube** (`nostr_signer`):
|
||||
```bash
|
||||
bash setup_signer_qube.sh # from packaging/qubes/
|
||||
~/.local/bin/nsigner --listen unix --socket-name nsigner --bridge-source-trusted
|
||||
```
|
||||
|
||||
**In dom0**:
|
||||
```bash
|
||||
bash setup_dom0.sh nostr_signer # from packaging/qubes/
|
||||
```
|
||||
|
||||
The dom0 policy allows trusted caller qubes without a popup (memory isolation is the real security boundary) and asks for confirmation from any other qube. The signer's own approval prompt at the `nostr_signer` terminal is the operation-level gate.
|
||||
|
||||
**From a caller qube**:
|
||||
```bash
|
||||
node examples/n_signer_qube_example_qrexec.js nostr_signer
|
||||
```
|
||||
|
||||
Setup scripts and policy are in [`packaging/qubes/`](packaging/qubes/). See also [`documents/QUBES_OS.md`](documents/QUBES_OS.md) and [`documents/qubes_client_examples.md`](documents/qubes_client_examples.md).
|
||||
|
||||
## 7. Usage
|
||||
|
||||
### 7.1 Run the program
|
||||
|
||||
```bash
|
||||
nsigner
|
||||
```
|
||||
|
||||
Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the TUI banner shows the randomly assigned signer name and its abstract socket address.
|
||||
Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the running status display shows the randomly assigned signer name and its abstract socket address.
|
||||
|
||||
To force a specific socket name (e.g. for scripted clients):
|
||||
|
||||
```bash
|
||||
nsigner --name my_test_signer
|
||||
```
|
||||
|
||||
Qubes/qrexec service mode (single framed request over stdin/stdout):
|
||||
|
||||
Other transport modes:
|
||||
```bash
|
||||
nsigner --listen qrexec
|
||||
nsigner --listen qrexec # Qubes qrexec (single framed request over stdin/stdout)
|
||||
nsigner --listen stdio # Generic stdio (single framed request over stdin/stdout)
|
||||
nsigner --listen tcp:[::]:11111 # FIPS/TCP (framed JSON, no TUI)
|
||||
nsigner --listen http:127.0.0.1:11111 # HTTP (curl-friendly, no TUI)
|
||||
```
|
||||
|
||||
Generic stdio transport mode (single framed request over stdin/stdout):
|
||||
|
||||
With OTP pad bound:
|
||||
```bash
|
||||
nsigner --listen stdio
|
||||
nsigner --listen http:127.0.0.1:11111 --otp-pad-dir /media/user/Music/pads --otp-pad 333e9902db839d9d --allow-all
|
||||
```
|
||||
|
||||
TCP transport mode (no TUI; serves requests until terminated):
|
||||
|
||||
Qrexec bridge mode (stateless relay to a persistent signer's unix socket):
|
||||
```bash
|
||||
nsigner --listen tcp:[::]:8080
|
||||
nsigner bridge --to nsigner
|
||||
```
|
||||
|
||||
### 9.2 Send a request (client mode)
|
||||
Persistent signer for qrexec bridge (unix listener with trusted source-qube preamble):
|
||||
```bash
|
||||
nsigner --listen unix --socket-name nsigner --bridge-source-trusted
|
||||
```
|
||||
|
||||
### 7.2 Send a request (client mode)
|
||||
|
||||
From another terminal, target the signer by its socket name:
|
||||
|
||||
```bash
|
||||
nsigner --socket-name nsigner_hairy_dog client '{"id":"1","method":"get_public_key","params":[]}'
|
||||
nsigner --socket-name nsigner_hairy_dog client '{"id":"1","method":"nostr_get_public_key","params":[]}'
|
||||
```
|
||||
|
||||
If only one signer is running you can omit the override and the client will use the default discovery rule.
|
||||
|
||||
Example signing request:
|
||||
|
||||
```bash
|
||||
nsigner -n nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
nsigner -n nsigner_hairy_dog client '{"id":"2","method":"nostr_sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
```
|
||||
|
||||
### 9.3 List running signers
|
||||
### 7.3 List running signers
|
||||
|
||||
```bash
|
||||
nsigner list
|
||||
```
|
||||
|
||||
Prints the abstract socket names of any currently running `nsigner` instances, e.g.:
|
||||
|
||||
```text
|
||||
@nsigner_hairy_dog
|
||||
@nsigner_brave_canyon
|
||||
```
|
||||
|
||||
### 9.4 Example session
|
||||
### 7.4 Example session
|
||||
|
||||
Terminal A:
|
||||
|
||||
```text
|
||||
$ nsigner
|
||||
[unlock] enter mnemonic:
|
||||
[ok] session unlocked
|
||||
signer name : hairy dog
|
||||
socket : @nsigner_hairy_dog
|
||||
[listen] unix-abstract:@nsigner_hairy_dog
|
||||
[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n)
|
||||
System is ready and waiting for connections on @nsigner_hairy_dog.
|
||||
[prompt] caller=uid:1000 method=nostr_sign_event role=main -> allow? (y/n)
|
||||
```
|
||||
|
||||
Terminal B:
|
||||
|
||||
```text
|
||||
$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"nostr_sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
{"id":"2","result":"<signed_event_json>"}
|
||||
```
|
||||
|
||||
## 10. Build and versioning
|
||||
## 8. Build and versioning
|
||||
|
||||
Build outputs a single executable artifact.
|
||||
|
||||
- [`build_static.sh`](build_static.sh): musl-static build path
|
||||
- [`Makefile`](Makefile): local build targets
|
||||
- [`Dockerfile.alpine-musl`](Dockerfile.alpine-musl): reproducible Alpine musl toolchain
|
||||
- [`resources/tui_continuous/tui_continuous.h`](resources/tui_continuous/tui_continuous.h) + [`resources/tui_continuous/tui_continuous.c`](resources/tui_continuous/tui_continuous.c): vendored continuous-TUI component (from `~/lt/aesthetics`, API baseline `TUI_CONTINUOUS_VERSION 0.0.9`)
|
||||
- [`increment_and_push.sh`](increment_and_push.sh): version/tag/release workflow
|
||||
- [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`)
|
||||
|
||||
Local dev build:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
./build/nsigner --version
|
||||
```
|
||||
|
||||
Static build:
|
||||
|
||||
```bash
|
||||
./build_static.sh
|
||||
./build/nsigner_static_x86_64 --version
|
||||
```
|
||||
|
||||
## 11. Document map
|
||||
|
||||
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md): client integration contract and framing behavior
|
||||
- [`documents/QUBES_OS.md`](documents/QUBES_OS.md): Qubes OS deployment/integration checklist for dedicated signer qubes
|
||||
- [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md): Tier-1 FIPS deployment runbook using loopback TCP listener
|
||||
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
|
||||
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
|
||||
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration
|
||||
|
||||
15
api.md
Normal file
15
api.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# n_signer API
|
||||
|
||||
The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API).
|
||||
|
||||
It covers:
|
||||
|
||||
- **§4.1 Request format** — JSON-RPC 2.0-style request shape.
|
||||
- **§4.2 Response format** — success/error shapes and the full error-code table.
|
||||
- **§4.3 Verbs** — the verb table (positional params + options), the `scheme` option for secp256k1, and the enforcement matrix.
|
||||
- **§4.4 Algorithms** — the algorithm table (secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp), derivation paths, key sizes, and the OTP one-time-pad model.
|
||||
- **§4.5 Examples** — worked request/response examples for every verb.
|
||||
- **§4.6 Role-based selectors** — `nostr_index` / `role` / `role_path` for the `nostr_*` verbs.
|
||||
- **§4.7 Pre-approval** — `--preapprove` syntax for algorithm-based and Nostr verbs.
|
||||
|
||||
For the security model, transports, and operational behavior, see [`README.md`](README.md) §1–§3 and §5–§8. For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
113
client/README.md
Normal file
113
client/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# n_signer C Client — migrated to `nostr_core_lib`
|
||||
|
||||
The hand-rolled `nsigner_client.{c,h}` that previously lived in this directory
|
||||
has been **removed**. n_signer now uses the shared, transport-pluggable client
|
||||
stack that lives in [`nostr_core_lib`](../resources/nostr_core_lib):
|
||||
|
||||
- [`nostr_core/nsigner_transport.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) —
|
||||
pluggable transport vtable (unix-abstract / tcp / usb-cdc serial / fds) + discovery.
|
||||
- [`nostr_core/nsigner_client.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) —
|
||||
heap-allocated client: 4-byte length-prefixed framing, kind-27235 auth envelope
|
||||
(self-contained), JSON-RPC verbs, cJSON result parsing, RPC error mapping.
|
||||
- [`nostr_core/nostr_signer.{h,c}`](../resources/nostr_core_lib/nostr_core/nostr_signer.h) —
|
||||
high-level `nostr_signer_t` abstraction (local + remote backends, 6 verbs).
|
||||
|
||||
This is the single source of truth for the n_signer wire contract. See
|
||||
[`nostr_core/NSIGNER_INTEGRATION.md`](../resources/nostr_core_lib/nostr_core/NSIGNER_INTEGRATION.md)
|
||||
for the full integration contract.
|
||||
|
||||
## What moved where
|
||||
|
||||
| Old (`client/`) | New (`nostr_core_lib`) |
|
||||
|---|---|
|
||||
| `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` |
|
||||
| `nsigner_client_init` / `connect_unix` / `close` | `nsigner_transport_open_unix` + `nsigner_client_new` / `nsigner_client_free` |
|
||||
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "nostr_get_public_key", ...)` |
|
||||
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "nostr_sign_event", ...)` |
|
||||
| `nsigner_client_set_auth` | `nsigner_client_set_auth` or `nostr_signer_nsigner_set_auth` |
|
||||
| `nsigner_client_request` / `request_raw` | `nsigner_client_call` (returns parsed cJSON result) |
|
||||
|
||||
## Consumers (updated)
|
||||
|
||||
- [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) — uses `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call`.
|
||||
- [`examples/sign_event_client.c`](../examples/sign_event_client.c) — same pattern.
|
||||
- [`tests/test_integration.c`](../tests/test_integration.c) — same pattern; cJSON-based assertions.
|
||||
|
||||
The `nsigner ... client '<json>'` subcommand in [`src/main.c`](../src/main.c) is
|
||||
unaffected — it has its own raw framing pass-through and never used this directory.
|
||||
|
||||
## Multi-Algorithm and Post-Quantum Verbs
|
||||
|
||||
n_signer supports six algorithms: `secp256k1` (Nostr), `ed25519` (SSH),
|
||||
`x25519` (age/ECDH), `ml-dsa-65` (PQ signatures, FIPS 204), `slh-dsa-128s`
|
||||
(PQ hash-based signatures, FIPS 205), and `ml-kem-768` (PQ KEM, FIPS 203).
|
||||
|
||||
The API has two verb families (see [`README.md`](../README.md#4-api) §4 for the full spec):
|
||||
|
||||
**Algorithm-based verbs** — the caller specifies `algorithm` and `index` in the
|
||||
options object. No role table entry is needed.
|
||||
|
||||
| Verb | Algorithms | Description |
|
||||
|---|---|---|
|
||||
| `get_public_key` | all key-deriving algorithms | Returns the derived public key (structured) |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature |
|
||||
| `encapsulate` | ml-kem-768 | KEM encapsulation with peer's public key |
|
||||
| `decapsulate` | ml-kem-768 | KEM decapsulation with derived private key |
|
||||
| `derive_shared_secret` | x25519 | ECDH key agreement |
|
||||
| `derive` | secp256k1 | `HMAC-SHA256(privkey, data)` — key-derived MAC for opaque identifiers (`index` required) |
|
||||
| `encrypt` / `decrypt` | otp | One-time pad encrypt/decrypt (`algorithm:"otp"`) |
|
||||
|
||||
**Nostr protocol verbs** — select a secp256k1 NIP-06 key via `nostr_index` (or
|
||||
`role`/`role_path`). These are role-based.
|
||||
|
||||
| Verb | Description |
|
||||
|---|---|
|
||||
| `nostr_get_public_key` | Returns the role's secp256k1 public key |
|
||||
| `nostr_sign_event` | Sign a Nostr event |
|
||||
| `nostr_mine_event` | NIP-13 PoW mining + sign |
|
||||
| `nostr_nip44_encrypt` / `nostr_nip44_decrypt` | NIP-44 encrypt/decrypt |
|
||||
| `nostr_nip04_encrypt` / `nostr_nip04_decrypt` | NIP-04 encrypt/decrypt |
|
||||
|
||||
Example: `nsigner_client_call(client, "sign", "[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]", &result)`
|
||||
|
||||
For secp256k1, the optional `scheme` parameter selects `"schnorr"` (default) or `"ecdsa"`.
|
||||
|
||||
### `get_public_key` response format
|
||||
|
||||
The algorithm-based `get_public_key` always returns a structured JSON string:
|
||||
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`.
|
||||
|
||||
The role-based `nostr_get_public_key` returns a plain 64-hex-char secp256k1
|
||||
public key by default, or the structured form with `{"format":"structured"}`.
|
||||
|
||||
Clients should parse the `result` string with `cJSON_Parse` to extract the
|
||||
`algorithm`, `public_key`, and `key_id` fields when the result is a JSON object.
|
||||
|
||||
### Key sizes
|
||||
|
||||
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | 32 B | 32 B | 64 B | — | — |
|
||||
| ed25519 | 32 B | 32 B | 64 B | — | — |
|
||||
| x25519 | 32 B | 32 B | — | — | 32 B |
|
||||
| ML-DSA-65 | 1952 B | 4032 B | 3309 B | — | — |
|
||||
| SLH-DSA-128s | 32 B | 64 B | 7856 B | — | — |
|
||||
| ML-KEM-768 | 1184 B | 2400 B | — | 1088 B | 32 B |
|
||||
|
||||
### Example clients
|
||||
|
||||
- [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) — ML-DSA-65 sign
|
||||
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768 encaps/decaps
|
||||
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519 SSH sign
|
||||
|
||||
See [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)
|
||||
section 11 for the full multi-algorithm specification, derivation paths, and
|
||||
example request/response transcripts.
|
||||
|
||||
## Why
|
||||
|
||||
Per [`plans/nsigner_integration_plan.md`](../resources/nostr_core_lib/plans/nsigner_integration_plan.md)
|
||||
(Phase 7): retire per-project hand-rolled clients in favor of the shared module
|
||||
in `nostr_core_lib`, so the wire contract has one implementation and downstream
|
||||
projects get unix/tcp/serial/fds transports for free.
|
||||
322
client/demo_c99.c
Normal file
322
client/demo_c99.c
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* demo_c99.c — comprehensive C99 demo for connecting to a running n_signer
|
||||
* via Qubes qrexec and performing all three core operations:
|
||||
*
|
||||
* 1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
*
|
||||
* Note: nostr_mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
|
||||
* See demo_javascript.js and demo_python.py for nostr_mine_event usage examples.
|
||||
* The high-level nostr_signer API does not yet wrap nostr_mine_event.
|
||||
*
|
||||
* This uses the high-level nostr_signer API from nostr_core_lib:
|
||||
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
|
||||
* - nostr_signer_nsigner_set_nostr_index() — select key by NIP-06 index
|
||||
* - nostr_signer_get_public_key() — get pubkey
|
||||
* - nostr_signer_sign_event() — sign an event
|
||||
* - nostr_signer_nip44_encrypt() — encrypt
|
||||
* - nostr_signer_nip44_decrypt() — decrypt
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer running in the target qube with --bridge-source-trusted
|
||||
* - qubes.NsignerRpc service installed in the target qube
|
||||
* - dom0 qrexec policy allowing this qube to call the service
|
||||
*
|
||||
* Build (from n_signer repo root):
|
||||
* make examples
|
||||
*
|
||||
* Usage:
|
||||
* ./build/demo_c99 <target_qube> [nostr_index]
|
||||
* ./build/demo_c99 nostr_signer 1
|
||||
*
|
||||
* If no nostr_index is given, defaults to 0.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "nip019.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* Helper: convert 32-byte hex pubkey to bech32 npub */
|
||||
static int hex_to_npub(const char *hex, char *out_npub, size_t out_sz) {
|
||||
unsigned char bytes[32];
|
||||
int i;
|
||||
|
||||
if (strlen(hex) != 64) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < 32; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
|
||||
return -1;
|
||||
}
|
||||
bytes[i] = (unsigned char)byte;
|
||||
}
|
||||
|
||||
return nostr_key_to_bech32(bytes, "npub", out_npub);
|
||||
}
|
||||
|
||||
/* Helper: print an error with a human-readable description */
|
||||
static void print_error(const char *operation, int rc) {
|
||||
const char *desc = "unknown error";
|
||||
switch (rc) {
|
||||
case NOSTR_ERROR_INVALID_INPUT:
|
||||
desc = "invalid input";
|
||||
break;
|
||||
case NOSTR_ERROR_CRYPTO_FAILED:
|
||||
desc = "crypto operation failed";
|
||||
break;
|
||||
case NOSTR_ERROR_IO_FAILED:
|
||||
desc = "I/O failed (transport error)";
|
||||
break;
|
||||
case NOSTR_ERROR_NETWORK_FAILED:
|
||||
desc = "network failed";
|
||||
break;
|
||||
case NOSTR_ERROR_NSIGNER_POLICY_DENIED:
|
||||
desc = "policy denied (caller not approved at signer terminal)";
|
||||
break;
|
||||
case NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED:
|
||||
desc = "index not in signer's whitelist";
|
||||
break;
|
||||
default:
|
||||
/* Try to print the numeric code */
|
||||
fprintf(stderr, " %s failed: error code %d\n", operation, rc);
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, " %s failed: %s (code %d)\n", operation, desc, rc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 1: Get a public key by nostr_index.
|
||||
* Returns the hex pubkey in `out_hex` (must be 65 bytes).
|
||||
*/
|
||||
static int demo_get_public_key(nostr_signer_t *signer, int nostr_index,
|
||||
char *out_hex, size_t hex_sz) {
|
||||
char npub[128];
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 1: get_public_key (nostr_index=%d) ===\n", nostr_index);
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, out_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("get_public_key", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (hex_to_npub(out_hex, npub, sizeof(npub)) == 0) {
|
||||
printf(" pubkey hex: %s\n", out_hex);
|
||||
printf(" npub: %s\n", npub);
|
||||
} else {
|
||||
printf(" pubkey hex: %s\n", out_hex);
|
||||
printf(" (npub conversion failed)\n");
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 2: Sign a Nostr event (kind 1 text note).
|
||||
* The signed event JSON is printed.
|
||||
*/
|
||||
static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) {
|
||||
cJSON *unsigned_event = NULL;
|
||||
cJSON *signed_event = NULL;
|
||||
char *signed_json = NULL;
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===\n");
|
||||
|
||||
/* Build an unsigned Nostr event (kind 1 text note) */
|
||||
unsigned_event = cJSON_CreateObject();
|
||||
if (unsigned_event == NULL) {
|
||||
fprintf(stderr, " failed to create event JSON\n");
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
|
||||
cJSON_AddStringToObject(unsigned_event, "content", "Hello from n_signer C99 demo!");
|
||||
cJSON_AddNumberToObject(unsigned_event, "created_at", (int)time(NULL));
|
||||
|
||||
/* tags: empty array */
|
||||
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_CreateArray());
|
||||
|
||||
/* pubkey: the signer will fill this in, but we include it for completeness */
|
||||
cJSON_AddStringToObject(unsigned_event, "pubkey", pubkey_hex);
|
||||
|
||||
printf(" Unsigned event:\n");
|
||||
{
|
||||
char *tmp = cJSON_PrintUnformatted(unsigned_event);
|
||||
if (tmp) {
|
||||
printf(" %s\n", tmp);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sign it */
|
||||
rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_sign_event", rc);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print the signed event */
|
||||
signed_json = cJSON_Print(signed_event);
|
||||
if (signed_json) {
|
||||
printf(" Signed event:\n");
|
||||
printf(" %s\n", signed_json);
|
||||
free(signed_json);
|
||||
}
|
||||
|
||||
/* Extract and show the signature and event id */
|
||||
{
|
||||
cJSON *id = cJSON_GetObjectItemCaseSensitive(signed_event, "id");
|
||||
cJSON *sig = cJSON_GetObjectItemCaseSensitive(signed_event, "sig");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
printf(" event id: %s\n", id->valuestring);
|
||||
}
|
||||
if (sig && cJSON_IsString(sig)) {
|
||||
printf(" signature: %s\n", sig->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(signed_event);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 3: NIP-44 encrypt and decrypt.
|
||||
* Encrypts a message to ourselves (using our own pubkey as the peer),
|
||||
* then decrypts it to verify round-trip.
|
||||
*/
|
||||
static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) {
|
||||
const char *plaintext = "Secret message from n_signer C99 demo!";
|
||||
char *ciphertext = NULL;
|
||||
char *decrypted = NULL;
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===\n");
|
||||
printf(" plaintext: \"%s\"\n", plaintext);
|
||||
printf(" peer pubkey: %s (self)\n", pubkey_hex);
|
||||
|
||||
/* Encrypt */
|
||||
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &ciphertext);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_nip44_encrypt", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
printf(" ciphertext: %s\n", ciphertext);
|
||||
|
||||
/* Decrypt (using our own pubkey as the sender) */
|
||||
rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext, &decrypted);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_nip44_decrypt", rc);
|
||||
free(ciphertext);
|
||||
return rc;
|
||||
}
|
||||
|
||||
printf(" decrypted: \"%s\"\n", decrypted);
|
||||
|
||||
/* Verify round-trip */
|
||||
if (strcmp(plaintext, decrypted) == 0) {
|
||||
printf(" ✓ Round-trip verified: plaintext matches decrypted\n");
|
||||
} else {
|
||||
printf(" ✗ Round-trip FAILED: plaintext does not match decrypted\n");
|
||||
rc = NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
free(ciphertext);
|
||||
free(decrypted);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *target_qube;
|
||||
const char *service_name = "qubes.NsignerRpc";
|
||||
int nostr_index = 0;
|
||||
nostr_signer_t *signer = NULL;
|
||||
char pubkey_hex[65];
|
||||
int rc;
|
||||
|
||||
/* Ignore SIGPIPE — qrexec subprocess may close pipes abruptly */
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
||||
fprintf(stderr, "Example: %s nostr_signer 1\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
target_qube = argv[1];
|
||||
if (argc > 2) {
|
||||
nostr_index = atoi(argv[2]);
|
||||
}
|
||||
|
||||
/* Initialize the crypto subsystem */
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("=== n_signer C99 Demo ===\n");
|
||||
printf("Target qube: %s\n", target_qube);
|
||||
printf("Service: %s\n", service_name);
|
||||
printf("nostr_index: %d\n", nostr_index);
|
||||
printf("\n");
|
||||
|
||||
/* Create a high-level signer backed by qrexec transport */
|
||||
printf("Connecting to n_signer via qrexec...\n");
|
||||
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
||||
if (signer == NULL) {
|
||||
fprintf(stderr, "Failed to create qrexec signer.\n");
|
||||
fprintf(stderr, "Is qrexec-client-vm available? Is the service installed?\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
printf("Connected.\n");
|
||||
|
||||
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
||||
rc = nostr_signer_nsigner_set_nostr_index(signer, nostr_index);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("set_nostr_index", rc);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Demo 1: Get public key */
|
||||
rc = demo_get_public_key(signer, nostr_index, pubkey_hex, sizeof(pubkey_hex));
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Demo 2: Sign an event */
|
||||
rc = demo_sign_event(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Demo 3: NIP-44 encrypt/decrypt */
|
||||
rc = demo_nip44(signer, pubkey_hex);
|
||||
|
||||
cleanup:
|
||||
printf("\n=== Summary ===\n");
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
printf("All demos completed successfully.\n");
|
||||
} else {
|
||||
printf("Demo failed with error code %d.\n", rc);
|
||||
}
|
||||
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return (rc == NOSTR_SUCCESS) ? 0 : 1;
|
||||
}
|
||||
268
client/demo_javascript.js
Normal file
268
client/demo_javascript.js
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* demo_javascript.js — comprehensive JavaScript demo for connecting to a
|
||||
* running n_signer via Qubes qrexec and performing all three core operations:
|
||||
*
|
||||
* 1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
*
|
||||
* Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
|
||||
* identity comes from QREXEC_REMOTE_DOMAIN on the server side.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer running in the target qube with --bridge-source-trusted
|
||||
* - qubes.NsignerRpc service installed in the target qube
|
||||
* - dom0 qrexec policy allowing this qube to call the service
|
||||
* - nostr-tools and @noble/secp256k1 npm packages installed
|
||||
*
|
||||
* Install dependencies (from n_signer repo root):
|
||||
* npm install nostr-tools @noble/secp256k1
|
||||
*
|
||||
* Usage:
|
||||
* node client/demo_javascript.js <target_qube> [nostr_index]
|
||||
* node client/demo_javascript.js nostr_signer 1
|
||||
*
|
||||
* If no nostr_index is given, defaults to 0.
|
||||
*/
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const secp = require("@noble/secp256k1");
|
||||
const { nip19 } = require("nostr-tools");
|
||||
|
||||
// @noble/secp256k1 v3 requires sync sha256/hmacSha256
|
||||
secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest());
|
||||
secp.hashes.hmacSha256 = (key, msg) =>
|
||||
new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest());
|
||||
|
||||
/**
|
||||
* Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one
|
||||
* framed response. Each call spawns a fresh qrexec-client-vm process.
|
||||
*
|
||||
* Framing: 4-byte big-endian length prefix + JSON payload.
|
||||
* No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN).
|
||||
*/
|
||||
function callNsigner(targetQube, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = Buffer.from(JSON.stringify(request), "utf8");
|
||||
const header = Buffer.alloc(4);
|
||||
header.writeUInt32BE(payload.length, 0);
|
||||
const framed = Buffer.concat([header, payload]);
|
||||
|
||||
const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
|
||||
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
||||
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`));
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
||||
reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`));
|
||||
return;
|
||||
}
|
||||
const buf = Buffer.concat(stdoutChunks);
|
||||
if (buf.length < 4) {
|
||||
reject(new Error("short response (missing frame header)"));
|
||||
return;
|
||||
}
|
||||
const len = buf.readUInt32BE(0);
|
||||
const body = buf.subarray(4, 4 + len);
|
||||
if (body.length !== len) {
|
||||
reject(new Error(`short response payload: expected ${len}, got ${body.length}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(body.toString("utf8")));
|
||||
} catch (e) {
|
||||
reject(new Error(`failed to parse response: ${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdin.write(framed);
|
||||
proc.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 1: Get a public key by nostr_index.
|
||||
*/
|
||||
async function demoGetPublicKey(targetQube, nostrIndex) {
|
||||
console.log(`\n=== Demo 1: get_public_key (nostr_index=${nostrIndex}) ===`);
|
||||
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "1",
|
||||
method: "get_public_key",
|
||||
params: [{ nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`get_public_key failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const pubkeyHex = response.result;
|
||||
const npub = nip19.npubEncode(pubkeyHex);
|
||||
console.log(` pubkey hex: ${pubkeyHex}`);
|
||||
console.log(` npub: ${npub}`);
|
||||
return pubkeyHex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 2: Sign a Nostr event (kind 1 text note).
|
||||
*/
|
||||
async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
|
||||
console.log("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===");
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
content: "Hello from n_signer JavaScript demo!",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
pubkey: pubkeyHex,
|
||||
};
|
||||
|
||||
console.log(" Unsigned event:");
|
||||
console.log(` ${JSON.stringify(unsignedEvent)}`);
|
||||
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "2",
|
||||
method: "nostr_nostr_sign_event",
|
||||
params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`nostr_sign_event failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const signedEvent = JSON.parse(response.result);
|
||||
console.log(" Signed event:");
|
||||
console.log(` ${JSON.stringify(signedEvent)}`);
|
||||
console.log(` event id: ${signedEvent.id}`);
|
||||
console.log(` signature: ${signedEvent.sig}`);
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 3: NIP-44 encrypt and decrypt.
|
||||
* Encrypts a message to ourselves (using our own pubkey as the peer),
|
||||
* then decrypts it to verify round-trip.
|
||||
*/
|
||||
async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
|
||||
const plaintext = "Secret message from n_signer JavaScript demo!";
|
||||
|
||||
console.log("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===");
|
||||
console.log(` plaintext: "${plaintext}"`);
|
||||
console.log(` peer pubkey: ${pubkeyHex} (self)`);
|
||||
|
||||
// Encrypt
|
||||
const encResponse = await callNsigner(targetQube, {
|
||||
id: "3",
|
||||
method: "nostr_nostr_nip44_encrypt",
|
||||
params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (encResponse.error) {
|
||||
throw new Error(`nostr_nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
|
||||
}
|
||||
|
||||
const ciphertext = encResponse.result;
|
||||
console.log(` ciphertext: ${ciphertext}`);
|
||||
|
||||
// Decrypt
|
||||
const decResponse = await callNsigner(targetQube, {
|
||||
id: "4",
|
||||
method: "nostr_nostr_nip44_decrypt",
|
||||
params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (decResponse.error) {
|
||||
throw new Error(`nostr_nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`);
|
||||
}
|
||||
|
||||
const decrypted = decResponse.result;
|
||||
console.log(` decrypted: "${decrypted}"`);
|
||||
|
||||
if (plaintext === decrypted) {
|
||||
console.log(" ✓ Round-trip verified: plaintext matches decrypted");
|
||||
} else {
|
||||
throw new Error("Round-trip FAILED: plaintext does not match decrypted");
|
||||
}
|
||||
}
|
||||
|
||||
async function demoMineEvent(targetQube, nostrIndex) {
|
||||
console.log("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---");
|
||||
|
||||
const event = {
|
||||
kind: 1,
|
||||
content: "Hello Nostr with PoW!",
|
||||
tags: [],
|
||||
};
|
||||
|
||||
console.log(" Mining with difficulty=4, threads=4, timeout_sec=30...");
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "5",
|
||||
method: "nostr_nostr_mine_event",
|
||||
params: [JSON.stringify(event), {
|
||||
difficulty: 4,
|
||||
threads: 4,
|
||||
timeout_sec: 30,
|
||||
nostr_index: nostrIndex,
|
||||
}],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`nostr_mine_event failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const result = JSON.parse(response.result);
|
||||
console.log(` achieved_difficulty: ${result.achieved_difficulty}`);
|
||||
console.log(` target_reached: ${result.target_reached}`);
|
||||
console.log(` elapsed_sec: ${result.elapsed_sec}`);
|
||||
console.log(` attempts: ${result.attempts}`);
|
||||
|
||||
const minedEvent = JSON.parse(result.event);
|
||||
console.log(` event id: ${minedEvent.id}`);
|
||||
console.log(` nonce tag: ${JSON.stringify(minedEvent.tags[0])}`);
|
||||
|
||||
if (result.target_reached) {
|
||||
console.log(" ✓ Target difficulty reached!");
|
||||
} else {
|
||||
console.log(` (Target not reached, best effort: ${result.achieved_difficulty} bits)`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targetQube = process.argv[2] || "nostr_signer";
|
||||
const nostrIndex = parseInt(process.argv[3] || "0", 10);
|
||||
|
||||
console.log("=== n_signer JavaScript Demo ===");
|
||||
console.log(`Target qube: ${targetQube}`);
|
||||
console.log(`Service: qubes.NsignerRpc`);
|
||||
console.log(`nostr_index: ${nostrIndex}`);
|
||||
console.log("\nConnecting to n_signer via qrexec...");
|
||||
|
||||
try {
|
||||
const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex);
|
||||
await demoSignEvent(targetQube, nostrIndex, pubkeyHex);
|
||||
await demoNip44(targetQube, nostrIndex, pubkeyHex);
|
||||
await demoMineEvent(targetQube, nostrIndex);
|
||||
|
||||
console.log("\n=== Summary ===");
|
||||
console.log("All demos completed successfully.");
|
||||
} catch (e) {
|
||||
console.error("\n=== Summary ===");
|
||||
console.error(`Demo failed: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
296
client/demo_python.py
Normal file
296
client/demo_python.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
demo_python.py — comprehensive Python demo for connecting to a running n_signer
|
||||
via Qubes qrexec and performing all three core operations:
|
||||
|
||||
1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
|
||||
Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
|
||||
identity comes from QREXEC_REMOTE_DOMAIN on the server side.
|
||||
|
||||
Prerequisites:
|
||||
- n_signer running in the target qube with --bridge-source-trusted
|
||||
- qubes.NsignerRpc service installed in the target qube
|
||||
- dom0 qrexec policy allowing this qube to call the service
|
||||
- Python 3 with no external dependencies (uses only stdlib)
|
||||
|
||||
Usage:
|
||||
python3 client/demo_python.py <target_qube> [nostr_index]
|
||||
python3 client/demo_python.py nostr_signer 1
|
||||
|
||||
If no nostr_index is given, defaults to 0.
|
||||
"""
|
||||
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
# --- Bech32 encoder (NIP-19 npub conversion, no external dependencies) ---
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def bech32_polymod(values):
|
||||
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
||||
chk = 1
|
||||
for v in values:
|
||||
b = chk >> 25
|
||||
chk = (chk & 0x1FFFFFF) << 5 ^ v
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((b >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_create_checksum(hrp, data):
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data):
|
||||
combined = data + bech32_create_checksum(hrp, data)
|
||||
return hrp + "1" + "".join([CHARSET[d] for d in combined])
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad and bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
return ret
|
||||
|
||||
|
||||
def hex_to_npub(pubkey_hex):
|
||||
"""Convert a 32-byte hex pubkey to bech32 npub format (NIP-19)."""
|
||||
pubkey_bytes = bytes.fromhex(pubkey_hex)
|
||||
data = convertbits(pubkey_bytes, 8, 5)
|
||||
return bech32_encode("npub", data)
|
||||
|
||||
|
||||
# --- n_signer qrexec client ---
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
"""
|
||||
Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one
|
||||
framed response. Each call spawns a fresh qrexec-client-vm process.
|
||||
|
||||
Framing: 4-byte big-endian length prefix + JSON payload.
|
||||
No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN).
|
||||
"""
|
||||
payload = json.dumps(request, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(frame)
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"qrexec-client-vm exited with code {proc.returncode}: "
|
||||
f"{err.decode('utf-8', 'replace').strip()}"
|
||||
)
|
||||
|
||||
if len(out) < 4:
|
||||
raise RuntimeError("short response (missing frame header)")
|
||||
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
body = out[4 : 4 + length]
|
||||
if len(body) != length:
|
||||
raise RuntimeError(
|
||||
f"short response payload: expected {length}, got {len(body)}"
|
||||
)
|
||||
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
|
||||
# --- Demos ---
|
||||
|
||||
def demo_get_public_key(target_qube, nostr_index):
|
||||
"""Demo 1: Get a public key by nostr_index."""
|
||||
print(f"\n=== Demo 1: get_public_key (nostr_index={nostr_index}) ===")
|
||||
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{"id": "1", "method": "get_public_key", "params": [{"nostr_index": nostr_index}]},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"get_public_key failed: {json.dumps(response['error'])}")
|
||||
|
||||
pubkey_hex = response["result"]
|
||||
npub = hex_to_npub(pubkey_hex)
|
||||
print(f" pubkey hex: {pubkey_hex}")
|
||||
print(f" npub: {npub}")
|
||||
return pubkey_hex
|
||||
|
||||
|
||||
def demo_sign_event(target_qube, nostr_index, pubkey_hex):
|
||||
"""Demo 2: Sign a Nostr event (kind 1 text note)."""
|
||||
print("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===")
|
||||
|
||||
unsigned_event = {
|
||||
"kind": 1,
|
||||
"content": "Hello from n_signer Python demo!",
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"pubkey": pubkey_hex,
|
||||
}
|
||||
|
||||
print(" Unsigned event:")
|
||||
print(f" {json.dumps(unsigned_event, separators=(',', ':'))}")
|
||||
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "2",
|
||||
"method": "nostr_nostr_sign_event",
|
||||
"params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"nostr_sign_event failed: {json.dumps(response['error'])}")
|
||||
|
||||
signed_event = json.loads(response["result"])
|
||||
print(" Signed event:")
|
||||
print(f" {json.dumps(signed_event, separators=(',', ':'))}")
|
||||
print(f" event id: {signed_event['id']}")
|
||||
print(f" signature: {signed_event['sig']}")
|
||||
return signed_event
|
||||
|
||||
|
||||
def demo_nip44(target_qube, nostr_index, pubkey_hex):
|
||||
"""Demo 3: NIP-44 encrypt and decrypt."""
|
||||
plaintext = "Secret message from n_signer Python demo!"
|
||||
|
||||
print("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===")
|
||||
print(f' plaintext: "{plaintext}"')
|
||||
print(f" peer pubkey: {pubkey_hex} (self)")
|
||||
|
||||
# Encrypt
|
||||
enc_response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "3",
|
||||
"method": "nostr_nostr_nip44_encrypt",
|
||||
"params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in enc_response:
|
||||
raise RuntimeError(f"nostr_nip44_encrypt failed: {json.dumps(enc_response['error'])}")
|
||||
|
||||
ciphertext = enc_response["result"]
|
||||
print(f" ciphertext: {ciphertext}")
|
||||
|
||||
# Decrypt
|
||||
dec_response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "4",
|
||||
"method": "nostr_nostr_nip44_decrypt",
|
||||
"params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in dec_response:
|
||||
raise RuntimeError(f"nostr_nip44_decrypt failed: {json.dumps(dec_response['error'])}")
|
||||
|
||||
decrypted = dec_response["result"]
|
||||
print(f' decrypted: "{decrypted}"')
|
||||
|
||||
if plaintext == decrypted:
|
||||
print(" ✓ Round-trip verified: plaintext matches decrypted")
|
||||
else:
|
||||
raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")
|
||||
|
||||
|
||||
def demo_nostr_mine_event(target_qube, nostr_index):
|
||||
print("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---")
|
||||
|
||||
event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []}
|
||||
|
||||
print(" Mining with difficulty=4, threads=4, timeout_sec=30...")
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "5",
|
||||
"method": "nostr_nostr_mine_event",
|
||||
"params": [json.dumps(event), {
|
||||
"difficulty": 4,
|
||||
"threads": 4,
|
||||
"timeout_sec": 30,
|
||||
"nostr_index": nostr_index,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"nostr_mine_event failed: {json.dumps(response['error'])}")
|
||||
|
||||
result = json.loads(response["result"])
|
||||
print(f" achieved_difficulty: {result['achieved_difficulty']}")
|
||||
print(f" target_reached: {result['target_reached']}")
|
||||
print(f" elapsed_sec: {result['elapsed_sec']}")
|
||||
print(f" attempts: {result['attempts']}")
|
||||
|
||||
mined_event = json.loads(result["event"])
|
||||
print(f" event id: {mined_event['id']}")
|
||||
print(f" nonce tag: {mined_event['tags'][0]}")
|
||||
|
||||
if result["target_reached"]:
|
||||
print(" ✓ Target difficulty reached!")
|
||||
else:
|
||||
print(f" (Target not reached, best effort: {result['achieved_difficulty']} bits)")
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
target_qube = sys.argv[1] if len(sys.argv) > 1 else "nostr_signer"
|
||||
nostr_index = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
|
||||
print("=== n_signer Python Demo ===")
|
||||
print(f"Target qube: {target_qube}")
|
||||
print(f"Service: qubes.NsignerRpc")
|
||||
print(f"nostr_index: {nostr_index}")
|
||||
print("\nConnecting to n_signer via qrexec...")
|
||||
|
||||
try:
|
||||
pubkey_hex = demo_get_public_key(target_qube, nostr_index)
|
||||
demo_sign_event(target_qube, nostr_index, pubkey_hex)
|
||||
demo_nip44(target_qube, nostr_index, pubkey_hex)
|
||||
demo_nostr_mine_event(target_qube, nostr_index)
|
||||
|
||||
print("\n=== Summary ===")
|
||||
print("All demos completed successfully.")
|
||||
except Exception as e:
|
||||
print("\n=== Summary ===")
|
||||
print(f"Demo failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,14 +6,21 @@ This document is the client-integration spec for `nsigner`.
|
||||
|
||||
It is written for agent/tool authors implementing robust request flows against the local signer process.
|
||||
|
||||
Reference implementation in this repository:
|
||||
|
||||
- Reusable C client library: `client/nsigner_client.h` + `client/nsigner_client.c`
|
||||
- Minimal usage examples: `examples/get_public_key_client.c` and `examples/sign_event_client.c`
|
||||
- Auth envelope builder used by the client: `src/auth_envelope.h` (`auth_envelope_build_for_request`)
|
||||
|
||||
---
|
||||
|
||||
## 2. Discovery and socket targeting
|
||||
|
||||
`nsigner` currently supports two transport families:
|
||||
`nsigner` currently supports three transport families:
|
||||
|
||||
- Linux AF_UNIX **abstract namespace** sockets.
|
||||
- Stdio framed mode (`--listen stdio` and `--listen qrexec`) for one request/response exchange.
|
||||
- TCP framed mode (`--listen tcp:IPv4:PORT` or `--listen tcp:[IPv6]:PORT`).
|
||||
|
||||
For AF_UNIX:
|
||||
|
||||
@@ -54,6 +61,24 @@ In server mode:
|
||||
|
||||
This mode is server-side only in the current CLI (the `client` subcommand still targets AF_UNIX).
|
||||
|
||||
### 2.4 qrexec authentication posture (`--auth`)
|
||||
|
||||
qrexec now supports configurable auth-envelope handling:
|
||||
|
||||
- `--listen qrexec --auth off` (default): legacy behavior. Requests are authorized as `qubes:<vm-name>` only.
|
||||
- `--listen qrexec --auth optional`: if the request includes an `auth` field, it is verified using the same auth envelope rules as TCP. On success, caller identity is upgraded to `qubes:<vm-name>+pubkey:<hex>`.
|
||||
- `--listen qrexec --auth required`: every request must carry a valid `auth` envelope, and caller identity is `qubes:<vm-name>+pubkey:<hex>`.
|
||||
|
||||
When `--auth optional` is used, a malformed or invalid `auth` object is rejected with auth-layer errors (`2010..2017`) rather than silently falling back to `qubes:<vm-name>`.
|
||||
|
||||
### 2.5 TCP mode authentication (required)
|
||||
|
||||
For TCP transport, requests MUST include an `auth` object containing a signed Nostr-style event envelope.
|
||||
|
||||
- Missing `auth` returns `{"error":{"code":2014,"message":"auth_envelope_required"}}`.
|
||||
- Signature verification, method/id/body binding, timestamp skew checks, and replay checks are enforced before policy lookup.
|
||||
- On success, caller identity is normalized to `pubkey:<hex>` for policy checks.
|
||||
|
||||
---
|
||||
|
||||
## 3. Transport framing
|
||||
@@ -102,6 +127,26 @@ Methods are NIP-46 style verbs.
|
||||
- `nip44_encrypt`
|
||||
- `nip44_decrypt`
|
||||
|
||||
### 4.2b Algorithm-based verbs (new)
|
||||
|
||||
In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly:
|
||||
|
||||
- `sign` — sign arbitrary bytes (params: `[message_hex, {algorithm, index, scheme?}]`)
|
||||
- `verify` — verify a signature (params: `[message_hex, signature_hex, {algorithm, index, scheme?}]`)
|
||||
- `encapsulate` — KEM encapsulation (params: `[peer_pubkey_hex, {algorithm}]`)
|
||||
- `decapsulate` — KEM decapsulation (params: `[ciphertext_hex, {algorithm, index}]`)
|
||||
- `derive_shared_secret` — ECDH key agreement (params: `[peer_pubkey_hex, {algorithm, index}]`)
|
||||
- `derive` — `HMAC-SHA256(privkey, data)` key-derived MAC (params: `[data, {algorithm:"secp256k1", index}]`; `index` required). Returns `{algorithm, key_id, digest}` where `digest` is 64 hex chars. Use for deterministic opaque identifiers (e.g. NIP-33 `d` tags) keyed by the derived private key.
|
||||
- `get_public_key` with `algorithm` parameter — returns structured JSON
|
||||
|
||||
Algorithm names: `secp256k1`, `ed25519`, `ml-dsa-65`, `slh-dsa-128s`, `x25519`, `ml-kem-768`
|
||||
|
||||
For secp256k1 `sign`/`verify`, the optional `scheme` parameter selects `"schnorr"` (default, BIP-340) or `"ecdsa"`.
|
||||
|
||||
Old verb aliases (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) map to the new verbs when used with the `algorithm` parameter. Without `algorithm`, they fall through to the role-based path.
|
||||
|
||||
See [README.md §4c](../README.md) for full details.
|
||||
|
||||
### 4.3 Selector options
|
||||
|
||||
The last param may include selector options:
|
||||
@@ -147,6 +192,14 @@ Representative error names clients must handle:
|
||||
- `unauthorized`
|
||||
- `approval_denied`
|
||||
- `internal_error`
|
||||
- `auth_envelope_malformed` (2010)
|
||||
- `auth_body_mismatch` (2011)
|
||||
- `auth_signature_invalid` (2012)
|
||||
- `auth_kind_invalid` (2013)
|
||||
- `auth_envelope_required` (2014)
|
||||
- `auth_envelope_mismatch` (2015)
|
||||
- `auth_envelope_stale` (2016)
|
||||
- `auth_replay_detected` (2017)
|
||||
|
||||
### 5.1 Recovery guidance
|
||||
|
||||
@@ -158,6 +211,7 @@ Representative error names clients must handle:
|
||||
- `unauthorized`: caller identity disallowed by policy; do not blind-retry.
|
||||
- `approval_denied`: user rejected prompt; treat as final unless user initiates retry.
|
||||
- `internal_error`: bounded retry with backoff; surface diagnostics.
|
||||
- `auth_envelope_*` / `auth_*` (2010-2017): fix request signing/auth envelope generation; do not blind-retry unchanged payloads.
|
||||
|
||||
---
|
||||
|
||||
@@ -450,7 +504,228 @@ Decrypt response:
|
||||
|
||||
---
|
||||
|
||||
## 11. Compatibility notes
|
||||
## 11. Post-Quantum and Multi-Algorithm Support
|
||||
|
||||
n_signer supports six cryptographic algorithms, all derived deterministically
|
||||
from the same BIP-39 mnemonic via distinct derivation paths:
|
||||
|
||||
| Algorithm | Purpose | Curve string | Purpose string | Derivation path |
|
||||
|---|---|---|---|---|
|
||||
| `secp256k1` | Nostr (sign_event, NIP-04/44) | `secp256k1` | `nostr` | `m/44'/1237'/<n>'/0/0` (NIP-06) |
|
||||
| `ed25519` | SSH signing, general signatures | `ed25519` | `ssh` | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) |
|
||||
| `x25519` | Key agreement (age, ECDH) | `x25519` | `age` | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) |
|
||||
| `ml-dsa-65` | Post-quantum signatures (FIPS 204) | `ml-dsa-65` | `pq-sig` | `m/44'/102003'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
| `slh-dsa-128s` | Post-quantum hash-based signatures (FIPS 205) | `slh-dsa-128s` | `pq-sig` | `m/44'/102004'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
| `ml-kem-768` | Post-quantum key encapsulation (FIPS 203) | `ml-kem-768` | `pq-kem` | `m/44'/102005'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
|
||||
The `102XXX` coin types are unregistered in SLIP-44 and reserved by n_signer
|
||||
for PQ/SSH/age algorithm families. All non-secp256k1 paths use SLIP-0010
|
||||
all-hardened derivation.
|
||||
|
||||
### 11.1 Algorithm key sizes
|
||||
|
||||
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | 32 bytes | 32 bytes | 64 bytes | — | — |
|
||||
| ed25519 | 32 bytes | 32 bytes | 64 bytes | — | — |
|
||||
| x25519 | 32 bytes | 32 bytes | — | — | 32 bytes |
|
||||
| ML-DSA-65 | 1952 bytes | 4032 bytes | 3309 bytes | — | — |
|
||||
| SLH-DSA-128s | 32 bytes | 64 bytes | 7856 bytes | — | — |
|
||||
| ML-KEM-768 | 1184 bytes | 2400 bytes | — | 1088 bytes | 32 bytes |
|
||||
|
||||
PQ public keys and signatures are much larger than classical ones. Clients
|
||||
must allocate buffers accordingly (ML-DSA-65 pubkey hex = 3904 chars;
|
||||
SLH-DSA-128s signature hex = 15712 chars; ML-KEM-768 pubkey hex = 2368 chars).
|
||||
|
||||
### 11.2 New verbs
|
||||
|
||||
| Verb | Purpose | Allowed (purpose, curve) | Description |
|
||||
|---|---|---|---|
|
||||
| `sign_data` | pq-sig, ssh | (pq-sig, ml-dsa-65), (pq-sig, slh-dsa-128s), (ssh, ed25519) | Sign arbitrary bytes (not a Nostr event) |
|
||||
| `verify_signature` | pq-sig, ssh | same as `sign_data` | Verify a signature against the role's public key |
|
||||
| `ssh_sign` | ssh | (ssh, ed25519) | Sign an SSH authentication challenge (ed25519) |
|
||||
| `kem_encapsulate` | pq-kem | (pq-kem, ml-kem-768) | Encapsulate: generate ciphertext + shared secret from a peer's ML-KEM public key |
|
||||
| `kem_decapsulate` | pq-kem | (pq-kem, ml-kem-768) | Decapsulate: recover shared secret from ciphertext using the role's ML-KEM private key |
|
||||
|
||||
The existing Nostr verbs (`sign_event`, `nip44_*`, `nip04_*`, `mine_event`)
|
||||
remain restricted to `purpose=nostr + curve=secp256k1`.
|
||||
|
||||
### 11.3 Structured `get_public_key` response format
|
||||
|
||||
`get_public_key` is a universal verb — it works for all six algorithms.
|
||||
|
||||
**For secp256k1 (backward compatibility):** the result is a plain hex string
|
||||
(the existing format). Existing Nostr clients are unaffected.
|
||||
|
||||
```json
|
||||
{ "id": "1", "result": "<64-char hex pubkey>" }
|
||||
```
|
||||
|
||||
**For secp256k1 with `format: "structured"` option:** new clients can request
|
||||
the structured format for consistency:
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{ "role": "main", "format": "structured" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"result": "{\"algorithm\":\"secp256k1\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}"
|
||||
}
|
||||
```
|
||||
|
||||
**For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s,
|
||||
ML-KEM-768):** the result is always a structured JSON object serialized as a
|
||||
string:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"result": {
|
||||
"algorithm": "ml-dsa-65",
|
||||
"public_key": "<hex-encoded public key>",
|
||||
"key_id": "<first 16 hex chars of public key>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `key_id` is the first 16 hex characters of the public key — a short
|
||||
display identifier similar to an SSH key fingerprint. The `result` field is a
|
||||
JSON string (the object serialized), so clients must parse it twice: once for
|
||||
the JSON-RPC envelope, once for the result object.
|
||||
|
||||
### 11.4 Example: `sign_data` (ML-DSA-65)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "10",
|
||||
"method": "sign_data",
|
||||
"params": ["68656c6c6f", { "role": "pq_sig" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "10",
|
||||
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ml-dsa-65\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the message bytes as hex. The signature is hex-encoded
|
||||
(3309 bytes = 6618 hex chars for ML-DSA-65).
|
||||
|
||||
### 11.5 Example: `verify_signature` (ed25519)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "11",
|
||||
"method": "verify_signature",
|
||||
"params": ["<msg_hex>", "<sig_hex>", { "role": "ssh_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "11", "result": "{\"valid\":true}" }
|
||||
```
|
||||
|
||||
The signature is verified against the role's derived public key.
|
||||
|
||||
### 11.6 Example: `ssh_sign` (ed25519)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"method": "ssh_sign",
|
||||
"params": ["<session_id_hex>", { "role": "ssh_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the SSH session ID (or challenge) as hex. The signature is
|
||||
a raw ed25519 signature (64 bytes = 128 hex chars).
|
||||
|
||||
### 11.7 Example: `kem_encapsulate` (ML-KEM-768)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "13",
|
||||
"method": "kem_encapsulate",
|
||||
"params": ["<peer_pubkey_hex>", { "role": "kem_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "13",
|
||||
"result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the peer's ML-KEM-768 public key as hex (1184 bytes = 2368
|
||||
hex chars). The response contains the ciphertext (1088 bytes = 2176 hex chars)
|
||||
and the shared secret (32 bytes = 64 hex chars). The encapsulating party keeps
|
||||
the shared secret; the ciphertext is sent to the decapsulating party.
|
||||
|
||||
### 11.8 Example: `kem_decapsulate` (ML-KEM-768)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "14",
|
||||
"method": "kem_decapsulate",
|
||||
"params": ["<ciphertext_hex>", { "role": "kem_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "14",
|
||||
"result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the ciphertext from `kem_encapsulate` (1088 bytes = 2176
|
||||
hex chars). The decapsulated shared secret will match the encapsulating
|
||||
party's shared secret.
|
||||
|
||||
### 11.9 Example clients
|
||||
|
||||
See the `examples/` directory for working C clients demonstrating the new
|
||||
verbs:
|
||||
|
||||
- [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) — ML-DSA-65
|
||||
`get_public_key` + `sign_data`
|
||||
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768
|
||||
`get_public_key` + `kem_encapsulate` + `kem_decapsulate` (verifies shared
|
||||
secrets match)
|
||||
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519
|
||||
`get_public_key` + `ssh_sign`
|
||||
|
||||
---
|
||||
|
||||
## 12. Compatibility notes
|
||||
|
||||
- If you are writing an autonomous agent client, pin to explicit socket name and explicit role selector.
|
||||
- Keep method support feature-detected (`method_not_found` fallback).
|
||||
|
||||
@@ -61,13 +61,13 @@ Operational assumptions:
|
||||
Run `nsigner` in TCP listen mode:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[::]:8080
|
||||
./build/nsigner --listen tcp:[::]:11111
|
||||
```
|
||||
|
||||
Or bind to a specific FIPS ULA address:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[fd00::1234]:8080
|
||||
./build/nsigner --listen tcp:[fd00::1234]:11111
|
||||
```
|
||||
|
||||
Behavior notes:
|
||||
|
||||
56
documents/N_OS_TR_AGENT_CHANGES.md
Normal file
56
documents/N_OS_TR_AGENT_CHANGES.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# n_OS_tr Agent Guide: Handling n_signer v0.0.16 Changes
|
||||
|
||||
This is a short operational guide for the n_OS_tr agent after the approval-model update in `n_signer`.
|
||||
|
||||
## What changed
|
||||
|
||||
- `n_signer` is now **deny-by-default**.
|
||||
- First-time caller/index combinations require approval unless preapproved.
|
||||
- `nostr_index` values can be auto-derived on approval.
|
||||
- Policy decisions are visible in activity labels: `:preapprove`, `:session-grant`, `:prompt`, `:no-match`.
|
||||
|
||||
## What the agent should do
|
||||
|
||||
1. **Always send explicit selector options** in the last `params` item:
|
||||
- Prefer `{"nostr_index": N}` for service identities.
|
||||
2. **Expect `policy_denied` on first contact** if caller/index is not preapproved.
|
||||
3. **Do not retry blindly** on `policy_denied`.
|
||||
- Surface a clear status: "approval needed".
|
||||
4. **Treat `unknown_role` as actionable**:
|
||||
- If using `nostr_index`, request again only after human approval or valid preapprove config.
|
||||
5. **Use stable caller identity** (service name / VM identity) so preapprove rules match reliably.
|
||||
|
||||
## Required startup pattern for n_OS_tr services
|
||||
|
||||
Launch `n_signer` with explicit `--preapprove` entries for each boot service that must sign without prompts.
|
||||
|
||||
Example:
|
||||
|
||||
```ini
|
||||
ExecStart=/usr/bin/n_signer \
|
||||
--preapprove caller=qubes:nostr-relay,role=main \
|
||||
--preapprove caller=qubes:dm-handler,nostr_index=1 \
|
||||
--preapprove caller=qubes:contacts,nostr_index=2 \
|
||||
--preapprove caller=qubes:zaps,nostr_index=3
|
||||
```
|
||||
|
||||
## Agent error-handling policy (recommended)
|
||||
|
||||
- `policy_denied` → stop, mark as "awaiting approval/preapprove".
|
||||
- `unknown_role` with `nostr_index` → stop, mark as "identity not yet approved/created".
|
||||
- `purpose_mismatch` / `curve_mismatch` → configuration bug; fail hard and alert.
|
||||
- `ambiguous_role_selector` → client bug; send exactly one selector field.
|
||||
|
||||
## Observability checks
|
||||
|
||||
When troubleshooting, inspect n_signer activity lines:
|
||||
|
||||
- `ALLOWED:preapprove` → expected for boot services.
|
||||
- `ALLOWED:session-grant` → interactive approval happened earlier this session.
|
||||
- `ALLOWED:prompt` → just approved now.
|
||||
- `DENIED:no-match` → no policy entry matched caller/index.
|
||||
|
||||
## Operational rule of thumb
|
||||
|
||||
For n_OS_tr system services, rely on `--preapprove` at startup.
|
||||
For ad-hoc/manual clients, rely on interactive prompt approvals.
|
||||
@@ -49,6 +49,16 @@ Still missing for complete Qubes integration:
|
||||
- qrexec policy in dom0 remains first enforcement boundary.
|
||||
- `n_signer` policy/approval remains second boundary.
|
||||
|
||||
### 3.4 Optional per-program identity inside one qube (`--auth optional`)
|
||||
|
||||
qrexec now supports an optional auth-envelope layer:
|
||||
|
||||
- `nsigner --listen qrexec --auth off` (default): identity remains `qubes:<source-vm>`.
|
||||
- `nsigner --listen qrexec --auth optional`: if a caller includes a valid auth envelope, identity becomes `qubes:<source-vm>+pubkey:<hex>`.
|
||||
- `nsigner --listen qrexec --auth required`: all requests must carry valid envelopes and identity is always `qubes:<source-vm>+pubkey:<hex>`.
|
||||
|
||||
This enables per-program approvals inside a single caller qube while preserving existing behavior for legacy callers that do not emit auth envelopes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Required implementation tasks
|
||||
@@ -102,6 +112,12 @@ In qrexec mode, default prompt behavior is now:
|
||||
|
||||
This replaces the previous permissive `PROMPT_NEVER` temporary setting.
|
||||
|
||||
If qrexec auth envelopes are enabled (`--auth optional` or `--auth required`), approved callers can also be recorded as composite identities:
|
||||
|
||||
- `qubes:<vm>+pubkey:<hex>`
|
||||
|
||||
This gives independent approval rows for distinct programs running in the same source qube.
|
||||
|
||||
## 4.4 Client helper examples ✅ Implemented
|
||||
|
||||
Added:
|
||||
@@ -135,6 +151,7 @@ Includes:
|
||||
- from caller qube, invoke test request (`get_public_key`)
|
||||
- confirm signer qube receives request
|
||||
- confirm activity log displays `qubes:<source-vm>` caller prefix
|
||||
- with `--auth optional`, confirm envelope-enabled clients show `qubes:<source-vm>+pubkey:<hex>`
|
||||
- validate deny behavior from unauthorized qube
|
||||
|
||||
## 5.4 Failure checks
|
||||
@@ -143,6 +160,58 @@ Includes:
|
||||
- missing policy -> deny path
|
||||
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
|
||||
|
||||
## 5.5 USB keyboard firmware workflows in a PCI-USB-controller qube
|
||||
|
||||
When a full USB controller is assigned directly to a qube via PCI passthrough, HID input does **not** flow through `sys-usb` input proxy policy. In this setup, keyboard-class USB blocking is usually caused by `usbguard` inside the owning qube.
|
||||
|
||||
### Scope check (dom0)
|
||||
|
||||
```bash
|
||||
qvm-prefs AI klass
|
||||
```
|
||||
|
||||
- `StandaloneVM`: service state changes in the qube root filesystem persist.
|
||||
- `AppVM`: root filesystem state resets at reboot; use `/rw/config/rc.local` for per-qube persistence.
|
||||
|
||||
### Disable usbguard immediately (inside AI qube)
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now usbguard
|
||||
sudo systemctl mask usbguard
|
||||
```
|
||||
|
||||
### Persist behavior across reboot
|
||||
|
||||
- **StandaloneVM**: the mask persists as-is.
|
||||
- **AppVM**: persist with `/rw/config/rc.local`:
|
||||
|
||||
```bash
|
||||
sudo tee /rw/config/rc.local >/dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
systemctl stop usbguard 2>/dev/null || true
|
||||
systemctl mask usbguard 2>/dev/null || true
|
||||
EOF
|
||||
sudo chmod +x /rw/config/rc.local
|
||||
```
|
||||
|
||||
### Restart + verify
|
||||
|
||||
```bash
|
||||
# dom0
|
||||
qvm-shutdown --wait AI && qvm-start AI
|
||||
|
||||
# inside AI qube
|
||||
systemctl is-enabled usbguard
|
||||
lsusb
|
||||
ls /dev/ttyACM*
|
||||
```
|
||||
|
||||
Expected result: `usbguard` is `masked`, the keyboard-class device enumerates, and serial/WebUSB workflows remain usable.
|
||||
|
||||
### Security trade-off
|
||||
|
||||
Disabling `usbguard` removes an important BadUSB defense for that qube. Keep this configuration limited to a dedicated hardware-development qube and avoid using it for general browsing or untrusted USB devices.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security requirements
|
||||
|
||||
606
documents/SECURITY.md
Normal file
606
documents/SECURITY.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# SECURITY.md
|
||||
|
||||
This document explains how `n_signer` enforces security, written so someone new can understand the model end-to-end without reading the source.
|
||||
|
||||
If you only read one section, read [§3 The three concepts](#3-the-three-concepts-identity-index-approval) and [§4 The deny-by-default rule](#4-the-deny-by-default-rule).
|
||||
|
||||
> This document describes the security model as implemented in the current release. Design rationale is in [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. What `n_signer` is, in security terms
|
||||
|
||||
`n_signer` is a **single foreground program** that holds a BIP-39 mnemonic in locked memory and signs requests on behalf of local clients. It is not a daemon, not a service, and not a key-management database.
|
||||
|
||||
The security posture is intentionally minimalist:
|
||||
|
||||
- **One process. One terminal. One human.** The terminal is the trust anchor and the only out-of-band approval surface.
|
||||
- **No persistence.** Nothing about the running session is written to disk: no config, no logs, no PID files, no socket pathnames, no state recovery.
|
||||
- **Crash equals total wipe.** All sensitive state — mnemonic, derived keys, the policy table, activity buffer — exists only in process RAM (`mlock`'d where applicable) and is unrecoverable after the process ends.
|
||||
- **Always-attended operation.** Every first request from a previously unknown caller requires an explicit human approval at the terminal, unless the caller was pre-approved at startup by the OS distribution. Human presence is part of the threat model, not an inconvenience.
|
||||
|
||||
Authoritative behavior reference: [`README.md`](../README.md). Implementation roadmap: [`plans/nsigner.md`](../plans/nsigner.md). Approval model plan: [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md). Wire contract for clients: [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md).
|
||||
|
||||
Interactive terminal presentation now follows the continuous-TUI conventions from `~/lt/aesthetics/TUI.md`, implemented via vendored [`resources/tui_continuous/tui_continuous.h`](../resources/tui_continuous/tui_continuous.h) / [`resources/tui_continuous/tui_continuous.c`](../resources/tui_continuous/tui_continuous.c) (baseline `TUI_CONTINUOUS_VERSION 0.0.9`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Threat model
|
||||
|
||||
### 2.1 What `n_signer` is designed to defend against
|
||||
|
||||
- **Untrusted local clients** asking the signer to perform operations they should not be allowed to perform.
|
||||
- **Drive-by signing** — an unattended program quietly producing signatures the user did not consent to.
|
||||
- **Accidental misuse** — a buggy client connecting to the wrong signer instance, or asking for an identity it should not be using.
|
||||
- **Post-mortem key recovery** — someone obtaining the disk after a crash, kill, or shutdown and recovering signing keys.
|
||||
|
||||
### 2.2 What `n_signer` does **not** defend against
|
||||
|
||||
- A compromised kernel or hypervisor (`mlock` cannot save you).
|
||||
- Physical access while the program is running and the terminal is unlocked.
|
||||
- A malicious user who knows the mnemonic — `n_signer` is a runtime gatekeeper, not a vault.
|
||||
- Side channels (timing, power, microarchitectural) that target the underlying crypto libraries.
|
||||
- Network adversaries when transports without authentication are explicitly enabled (e.g. `tcp_local`).
|
||||
- An OS distribution that ships a malicious `--preapprove` list in its system unit files. The OS is in scope of trust if it pre-approves anything; see [§7](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
### 2.3 Trust anchors
|
||||
|
||||
| Anchor | Why it is trusted |
|
||||
|---|---|
|
||||
| The terminal `n_signer` is attached to | The user types the mnemonic into it and answers approval prompts on it. If the terminal is compromised, the session is compromised. |
|
||||
| The kernel and `mlock` / `getrandom` syscalls | Used for in-memory protection and entropy. |
|
||||
| The static binary itself | Built reproducibly via [`build_static.sh`](../build_static.sh) and shipped as one musl-static artifact. |
|
||||
| The launcher that started `n_signer` | Whoever ran the process chose the `--preapprove` flags. In interactive use that's the human; in `n_OS_tr` boot it's the OS init system. The launcher's integrity is part of the trust chain. |
|
||||
| Parent process (when using `--mnemonic-stdin` / `--mnemonic-fd`) | In non-interactive startup mode, the parent provides mnemonic bytes over stdin/inherited FD. That parent is now explicitly in the trust chain for mnemonic handling correctness and secrecy. |
|
||||
|
||||
Everything else — clients, other processes, other users, remote callers — is **untrusted by default** and must clear an approval check.
|
||||
|
||||
---
|
||||
|
||||
## 3. The three concepts: identity, index, approval
|
||||
|
||||
This is the entire user-facing security model. There are exactly three things to know.
|
||||
|
||||
```text
|
||||
+----------------+ +-----------------------+ +---------------+
|
||||
| IDENTITY | | INDEX (or PATH) | | APPROVAL |
|
||||
| who is asking | +---- | which key to use | ----+ | is this OK? |
|
||||
+----------------+ | +-----------------------+ | +---------------+
|
||||
| |
|
||||
| request resolves to |
|
||||
+-------> (identity, index/path) -----+
|
||||
|
|
||||
v
|
||||
+----------------------------+
|
||||
| Approved entry exists? |
|
||||
+----------------------------+
|
||||
|yes |no
|
||||
v v
|
||||
+-----------------+ +----------------+
|
||||
| Sign / encrypt | | Prompt human |
|
||||
+-----------------+ +----------------+
|
||||
|
|
||||
[y]/[a] | [n]
|
||||
allow now | deny
|
||||
/save |
|
||||
v v
|
||||
+-----------------+ +----------------+
|
||||
| Sign / encrypt | | policy_denied |
|
||||
+-----------------+ +----------------+
|
||||
```
|
||||
|
||||
### Identity — *who is asking*
|
||||
|
||||
An identity is a tagged caller record built from the transport. Different transports produce different identity tags; the signer never invents identity, it always reads it from the kernel or framework.
|
||||
|
||||
| Kind | What it carries | Source of truth |
|
||||
|---|---|---|
|
||||
| `unix_peer` | `uid`, `pid` from `SO_PEERCRED` on an abstract Unix socket. | Kernel — the client cannot forge it. |
|
||||
| `qubes` | source qube name from `QREXEC_REMOTE_DOMAIN`. | Qubes RPC framework. |
|
||||
| `qubes_pubkey` | source qube + verified auth-envelope pubkey (`qubes:<vm>+pubkey:<hex>`). | Qubes RPC framework + application-layer signature verification. |
|
||||
| `tcp_local` | Loopback address of caller. | TCP socket peer address — opt-in transport. |
|
||||
| `tcp_remote` | Verified auth-envelope pubkey (`pubkey:<hex>`). | Application-layer authentication. |
|
||||
| `fips` | Peer npub from a NIP-46 style flow (planned). | Application-layer authentication. |
|
||||
| `usb_serial` | Device path plus an asserted caller (planned). | Trust-on-first-use. |
|
||||
|
||||
Identity quality varies. `unix_peer` and `qubes` are vouched for by the kernel or hypervisor — strong. `qubes_pubkey` is stronger still for multi-program qubes because it combines hypervisor-vouched source-qube identity with a per-program cryptographic identity. `tcp_local` and `tcp_remote` rely on transport/app-layer checks and are only as strong as deployment choices.
|
||||
|
||||
### Index (or path) — *which key*
|
||||
|
||||
The signer can derive many keys from one mnemonic. Clients select one in two equivalent ways:
|
||||
|
||||
- `nostr_index = N` — shorthand for the Nostr derivation `m/44'/1237'/N'/0/0` per NIP-06.
|
||||
- `role_path = "<full BIP-32 path>"` — used for non-Nostr key trees (Bitcoin, SSH, etc.). Pre-registered only; see [§9.2](#92-pre-registration-of-role_path).
|
||||
|
||||
The three legacy selector words from the wire protocol — `role`, `nostr_index`, `role_path` — all resolve to a single internal record. From the user's perspective, what matters is "which Nostr identity (by index)" or "which advanced derivation (by path)."
|
||||
|
||||
|
||||
### Approval — *is this OK?*
|
||||
|
||||
An approval is an in-memory entry that says: *"this identity may use this index/path."* Approvals come from exactly two sources:
|
||||
|
||||
1. **The human at the prompt.** When a request arrives that has no matching approval, the signer blocks the request and asks the user. On `[a]` ("allow this caller for this index/path for the rest of the session") the signer adds a session approval. On `[y]` it allows once without saving. On `[n]` it denies.
|
||||
2. **Pre-approvals.** When the OS distribution starts `n_signer` with `--preapprove caller=...,nostr_index=...` flags, those become approvals that exist before any request arrives. They never trigger prompts. See [§7](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
Approvals are stored in an in-memory table and vanish on process exit. There is no persistent approval database.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. The deny-by-default rule
|
||||
|
||||
`n_signer` denies any request whose `(identity, index)` pair has no approval. Period.
|
||||
|
||||
There is no special case for "same-uid" callers. There is no implicit grant for the user running their own client. Every first request from every caller goes through an approval — either granted by the user at the prompt, or pre-approved by the OS distribution at startup.
|
||||
|
||||
This is the single most important property:
|
||||
|
||||
- It removes the assumption that "same-uid means trusted" — an assumption that was always more convenience than security.
|
||||
- It collapses several mechanisms (policy, role pre-registration, the `[a]` flag) into one (the in-memory approval table).
|
||||
- It makes the audit trail trivial: every signed operation maps to either a pre-approval declaration or a logged human approval.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. The two checks every request must pass
|
||||
|
||||
Every signing request runs through this short pipeline. Failure at any step returns an error response and is logged.
|
||||
|
||||
```text
|
||||
+----------------------------------------------------------------+
|
||||
| 1. CLIENT |
|
||||
| sends length-prefixed JSON-RPC request |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 2. TRANSPORT |
|
||||
| - reads framed bytes off the socket |
|
||||
| - builds caller_identity_t (uid / qubes vm / tcp peer / ...)|
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 3. RESOLVE INDEX |
|
||||
| - parse selector options from request |
|
||||
| - resolve to a specific (index or path) |
|
||||
| - reject ambiguous selectors |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 4. APPROVAL CHECK |
|
||||
| - is there an approval for (this identity, this index)? |
|
||||
| yes -> ALLOW |
|
||||
| no -> PROMPT human (or DENY if non-interactive) |
|
||||
| - on [a], append a session approval and continue |
|
||||
| - on [y], continue this request only |
|
||||
| - on [n] / EOF, deny |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 5. ENFORCEMENT (verb x purpose x curve) |
|
||||
| - is this verb compatible with the key's declared purpose? |
|
||||
| - reject with purpose_mismatch / curve_mismatch otherwise |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 6. KEY DERIVATION + SIGN |
|
||||
| - derive key from mnemonic at the resolved path |
|
||||
| - perform crypto operation |
|
||||
| - return signed result |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 7. RESPONSE FRAMED + SENT |
|
||||
| - activity log line appended with the approval source label |
|
||||
+----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
There are two independent gates: the **approval check** (does this caller have permission to use this key?) and the **enforcement check** (is this operation compatible with this key?). Both must pass.
|
||||
|
||||
### 5.1 Approval check — *does this caller have permission?*
|
||||
|
||||
Implemented as a lookup in the in-memory approval table. First match wins; the catch-all is `*` → deny. No approval entry, no signature.
|
||||
|
||||
The approval check answers *who* and *which key*. It does not check what the request will do with the key — that is the enforcement check.
|
||||
|
||||
### 5.2 Enforcement check — *is this operation compatible with this key?*
|
||||
|
||||
The signer enforces a strict `(verb, purpose, curve)` matrix:
|
||||
|
||||
| Verb | Required purpose | Required curve |
|
||||
|---|---|---|
|
||||
| `sign_event` | `nostr` | `secp256k1` |
|
||||
| `mine_event` | `nostr` | `secp256k1` |
|
||||
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
|
||||
| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` |
|
||||
| `get_public_key` | any | any (must match role's declared curve) |
|
||||
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
|
||||
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
|
||||
| `ssh_sign` | `ssh` | `ed25519` |
|
||||
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` |
|
||||
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` |
|
||||
| Any other verb | rejected | rejected |
|
||||
|
||||
A pre-approval to use a Bitcoin-purposed key for `sign_event` does **not** override the enforcement matrix. The approval grants access to the key; enforcement still gates the verb. **Fail-closed**: unknown verbs and unlisted `(verb, purpose, curve)` combinations are rejected, never passed through.
|
||||
|
||||
This is the layer that prevents (for example) a `bitcoin/secp256k1` key from being used to sign a Nostr event even if some pre-approval entry mistakenly named it. The key's *purpose* is part of its identity; you cannot reuse it across domains.
|
||||
|
||||
Test coverage: [`tests/test_enforcement.c`](../tests/test_enforcement.c).
|
||||
|
||||
---
|
||||
|
||||
## 6. The interactive approval prompt
|
||||
|
||||
When a request has no matching approval and `n_signer` is running attached to a terminal, the signer blocks the request, prints a prompt, and waits for a local keystroke.
|
||||
|
||||
Target prompt format:
|
||||
|
||||
```
|
||||
Approval required
|
||||
caller: qubes:nostr-relay
|
||||
verb: sign_event
|
||||
index: nostr_index=0
|
||||
purpose: nostr / secp256k1
|
||||
|
||||
[y] allow once
|
||||
[a] allow this caller for index 0 for the rest of the session
|
||||
[n] deny
|
||||
```
|
||||
|
||||
The keys mean:
|
||||
|
||||
- `[y]` — Allow this single request. No approval is saved; the next request from the same caller will prompt again.
|
||||
- `[a]` — Allow, and add a session approval so the same `(caller, index)` pair is silently allowed for the rest of the session.
|
||||
- `[n]` — Deny this request. Returns `policy_denied` to the client.
|
||||
- EOF / non-interactive — Treated as deny. There is no implicit "allow when nobody is at the terminal" mode. OS distributions that need silent operation use [`--preapprove`](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
### 6.1 What `[a]` does and does not do
|
||||
|
||||
- It approves the **specific caller** (e.g. `qubes:nostr-relay`).
|
||||
- It approves the **specific index/path** that triggered the prompt.
|
||||
- It does **not** approve other callers.
|
||||
- It does **not** approve other indices for the same caller — a different `nostr_index` from the same caller will prompt again.
|
||||
- It does **not** persist past the process lifetime.
|
||||
|
||||
|
||||
### 6.2 New-identity warning
|
||||
|
||||
When a request specifies an unregistered `nostr_index`, the prompt says so plainly:
|
||||
|
||||
```
|
||||
Approval required
|
||||
caller: qubes:nostr-relay
|
||||
verb: get_public_key
|
||||
index: nostr_index=7 (NEW IDENTITY — will be derived if approved)
|
||||
|
||||
[y] allow once [a] allow caller for this new identity [n] deny
|
||||
```
|
||||
|
||||
A new identity is just a new derivation of the same mnemonic. It is not a separate key file or a separate seed; it is `m/44'/1237'/7'/0/0` of the existing mnemonic. The user should still see explicitly that they are *creating* a previously-unused identity.
|
||||
|
||||
### 6.3 Race-condition behavior
|
||||
|
||||
Requests are served one at a time. If a malicious client tries to flood requests during a prompt, only the request being prompted is held; subsequent requests queue or are dropped per the transport's framing. The prompt always names the request being approved. It is not possible for a queued request to "ride along" on an approval intended for an earlier request.
|
||||
|
||||
---
|
||||
|
||||
## 7. Pre-approvals: the OS distribution pathway
|
||||
|
||||
For deployments where multiple system services need keys at boot — `n_OS_tr` and similar — the OS distribution starts `n_signer` with one `--preapprove` flag per service:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/n_signer \
|
||||
--preapprove caller=qubes:nostr-relay,role=main \
|
||||
--preapprove caller=qubes:dm-handler,nostr_index=1 \
|
||||
--preapprove caller=qubes:contacts,nostr_index=2 \
|
||||
--preapprove caller=qubes:zaps,nostr_index=3
|
||||
```
|
||||
|
||||
At startup, `n_signer`:
|
||||
|
||||
1. Parses each flag into an approval entry with prompt mode `NEVER`.
|
||||
2. Auto-derives any keys those entries depend on (so services don't see "ready" until keys exist).
|
||||
3. Writes one log line per pre-approval to stderr so the OS unit's journal records what was authorized.
|
||||
4. Begins listening normally. Pre-approved requests are served silently; everything else still hits the prompt or the catch-all deny.
|
||||
|
||||
### 7.1 What pre-approvals can do
|
||||
|
||||
- Allow a specific named caller to use a specific named key for any verb that key's purpose/curve permits.
|
||||
- Auto-create a Nostr identity at a specified `nostr_index` if it does not yet exist.
|
||||
- Be inspected: anyone reading the systemd unit (or `ps` output) can see exactly what was authorized.
|
||||
|
||||
### 7.2 What pre-approvals cannot do
|
||||
|
||||
- **Cannot deny.** Pre-approvals only widen authorization, never narrow it. The user at the terminal is always the source of denial. (The plan deliberately rejects "preapprove deny" entries; see [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) §C.3.)
|
||||
- **Cannot wildcard the caller.** Each entry must name exactly one caller. There is no `qubes:*` rule. The OS distribution must know who its services are.
|
||||
- **Cannot bypass enforcement.** A pre-approval to use a Nostr key for a hypothetical Bitcoin verb still fails the enforcement matrix in [§5.2](#52-enforcement-check--is-this-operation-compatible-with-this-key).
|
||||
- **Cannot pre-register `role_path` derivations.** Only `role=<name>` (an existing role) and `nostr_index=<n>` are allowed in pre-approval specs. Free-form `role_path` derivations remain pre-registration-only by code path. See [§9.2](#92-pre-registration-of-role_path).
|
||||
|
||||
### 7.3 Trust implications
|
||||
|
||||
Pre-approvals shift the trust boundary. Whoever wrote the systemd unit now decides what the signer will allow without asking. Concretely:
|
||||
|
||||
- The unit file's integrity matters. If an attacker can rewrite it, they can pre-approve themselves.
|
||||
- The OS launcher's identity-stamping must be trustworthy. If something else can spoof a `qubes:nostr-relay` tag, it inherits that pre-approval.
|
||||
- The user retains final authority at the running terminal. Anything *not* pre-approved still goes through the human-approved prompt.
|
||||
|
||||
In `n_OS_tr` these properties are part of the OS itself: the unit files are owned by root, the qrexec framework stamps caller identity at the hypervisor level, and the user's terminal is the ultimate arbiter. The trust chain is short and visible.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. The audit log
|
||||
|
||||
Every served (or denied) request appends a line to the in-memory activity buffer that the running TUI displays. Each line includes the **source label** that explains why the verdict was reached.
|
||||
|
||||
Target line format:
|
||||
|
||||
```
|
||||
[2026-05-04 16:03:11] qubes:nostr-relay sign_event(nostr_index=0) ALLOWED:preapprove
|
||||
[2026-05-04 16:03:14] uid:1000 sign_event(nostr_index=0) ALLOWED:session-grant
|
||||
[2026-05-04 16:03:18] tcp:[::1]:54122 get_public_key(nostr_index=7) ALLOWED:prompt
|
||||
[2026-05-04 16:03:21] uid:1001 sign_event(nostr_index=0) DENIED:no-match
|
||||
```
|
||||
|
||||
Source labels:
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| `:preapprove` | Approval came from a `--preapprove` startup flag. |
|
||||
| `:session-grant` | Approval was added earlier this session by a prompt `[a]`. |
|
||||
| `:prompt` | Approval was given just now by the user pressing `[y]` or `[a]` at the prompt. |
|
||||
| `:no-match` | No approval existed and (in non-interactive mode) the request was denied. |
|
||||
|
||||
This makes forensic review fast: every signed operation maps to either a pre-approval (audit the unit file), a session grant (audit the session's earlier prompt activity), or a fresh prompt approval (audit human attention at that moment).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 9. The role abstraction (internal plumbing)
|
||||
|
||||
Internally `n_signer` uses a structure called a **role** to bundle three things together:
|
||||
|
||||
- A derivation path (BIP-32).
|
||||
- A purpose label (`nostr`, `bitcoin`, `ssh`, `age`).
|
||||
- A curve label (`secp256k1`, `ed25519`, `x25519`).
|
||||
|
||||
Roles are the unit of *enforcement* (the matrix in [§5.2](#52-enforcement-check--is-this-operation-compatible-with-this-key) is keyed on role purpose and curve) and the unit of *audit* (every log line names the role used).
|
||||
|
||||
### 9.1 Why roles exist as plumbing rather than user-facing concept
|
||||
|
||||
- A future where one mnemonic produces Nostr, Bitcoin, SSH, and age keys requires *something* to track per-key purpose. Roles are that thing. Keeping them in the codebase preserves the option without forcing the user to think about them today.
|
||||
- Two different identities can use the same role (the same key). The role is the audit unit shared between them, so the activity log is consistent regardless of caller.
|
||||
- Enforcement against a role's declared purpose/curve is a defense-in-depth: a Nostr key cannot be coerced into producing a Bitcoin signature even if some approval entry mistakenly named it.
|
||||
|
||||
A user reading prompts and the activity log will see "index 0" and "index 7" most of the time, not "role main" or "role nostr_idx_7." Roles surface as labels only in advanced views and the source code.
|
||||
|
||||
### 9.2 Pre-registration of `role_path`
|
||||
|
||||
The `role_path` selector lets a client specify a BIP-32 path directly, including paths outside the Nostr derivation scheme. These remain **pre-registration-only**:
|
||||
|
||||
- The signer ships with an empty `role_path` table.
|
||||
- Operators who want non-Nostr keys must register them at startup (mechanism TBD).
|
||||
- The interactive prompt does **not** auto-derive `role_path` keys, even with `[a]`. The narrowing in [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) §5 explicitly applies only to `nostr_index`.
|
||||
|
||||
The reasoning is that `role_path` is free-form. A malicious or buggy client could request paths that overlap with sensitive domains (bitcoin wallet roots, SSH host keys, age recipients). Auto-derivation in those domains would be a footgun. `nostr_index` is locked to a specific scheme with locked purpose and curve, so auto-derivation there is safe to gate on a single human keystroke.
|
||||
|
||||
### 9.3 Role table size limits
|
||||
|
||||
`ROLE_TABLE_MAX_ENTRIES` caps the number of distinct roles per session. The plan raises this to 256. Overflow is reported as a clear `role_table_full` error rather than silent truncation or memory unsafety.
|
||||
|
||||
---
|
||||
|
||||
## 10. Memory safety and zeroization
|
||||
|
||||
### 10.1 Locked memory
|
||||
|
||||
- The mnemonic and derived private keys live in `mlock`'d allocations managed by [`src/secure_mem.c`](../src/secure_mem.c).
|
||||
- These pages are pinned in RAM (no swap to disk) for the process lifetime.
|
||||
- On normal shutdown (`q`, `SIGINT`, `SIGTERM`), buffers are explicitly zeroized before unmap.
|
||||
|
||||
### 10.2 Crash semantics
|
||||
|
||||
If the process dies abnormally, the kernel reclaims its pages. Anything previously `mlock`'d is not flushed to disk because it never could be. There is no swap residue to scrape, no core dump containing keys (if your environment disables core dumps for setuid-equivalent processes — verify locally), and no recovery path.
|
||||
|
||||
This is the meaning of "crash equals total wipe is a security feature" in [`plans/nsigner.md:154`](../plans/nsigner.md:154).
|
||||
|
||||
### 10.3 No persistence by design
|
||||
|
||||
There is no file under `~/.nsigner`, no `/var/lib/nsigner`, no `~/.config/nsigner`, no approval database. The signer is a process whose authority begins when you type the mnemonic and ends when you quit.
|
||||
|
||||
If you want something durable: store the mnemonic on paper or hardware, not in `n_signer`.
|
||||
|
||||
The only file inputs are `--preapprove` flags from the launcher's command line, evaluated once at startup and never re-read. There is no live config reload.
|
||||
|
||||
---
|
||||
|
||||
## 11. Transports and their identity quality
|
||||
|
||||
Different transports give different identity quality. The wire format (4-byte length prefix + JSON-RPC) and the security checks ([§5](#5-the-two-checks-every-request-must-pass)) are identical across transports.
|
||||
|
||||
### 11.1 Abstract namespace Unix sockets (default)
|
||||
|
||||
- Address: `@nsigner_<word1>_<word2>` per [`plans/nsigner.md:96`](../plans/nsigner.md:96).
|
||||
- Identity: kernel-vouched `uid`, `pid`. Cannot be forged by the client.
|
||||
- Risk surface: any local process on the same host. Deny-by-default + per-(caller, index) approvals handle this.
|
||||
|
||||
### 11.2 Stdio / qrexec mode
|
||||
|
||||
- One framed request, one framed response, then exit.
|
||||
- Identity defaults to `qubes:<vm-name>` from `QREXEC_REMOTE_DOMAIN`, vouched for by Qubes.
|
||||
- With `--listen qrexec --auth optional|required`, requests carrying a valid auth envelope are upgraded to `qubes:<vm-name>+pubkey:<hex>`.
|
||||
- Risk surface: only what Qubes RPC policy permits. See [`packaging/qubes/`](../packaging/qubes/).
|
||||
|
||||
### 11.3 TCP (advanced / opt-in)
|
||||
|
||||
- Loopback and remote TCP callers must present a valid signed auth envelope.
|
||||
- Identity is normalized to caller `pubkey:<hex>` after signature verification; TCP peer address is context only.
|
||||
- The auth gate runs before policy: missing/invalid envelopes are rejected with `2010..2017` auth errors.
|
||||
- Prompt-driven approval still applies after auth; auth answers "who is calling", policy answers "is this caller allowed".
|
||||
|
||||
### 11.4 Future transports
|
||||
|
||||
The roadmap ([`plans/nsigner.md:178`](../plans/nsigner.md:178)) describes additional transports (USB serial, FIPS-style relay). The contract is:
|
||||
|
||||
- The transport produces a `caller_identity_t`.
|
||||
- The same approval / enforcement / dispatch stack runs unchanged.
|
||||
- New transports add new identity *kinds*, not new bypass paths.
|
||||
|
||||
---
|
||||
|
||||
## 12. Operational guidance
|
||||
|
||||
### 12.1 Running interactively (typical desktop use)
|
||||
|
||||
- Start `n_signer` only on a terminal you control. No `--preapprove` flags.
|
||||
- Type or generate the mnemonic. Generated mnemonics are shown once with no confirmation step ([`plans/nsigner.md:84`](../plans/nsigner.md:84)) — write them down before pressing a key.
|
||||
- Stay near the terminal for the first couple of minutes after launch. New clients will prompt for approval. Press `[a]` to grant for the session.
|
||||
- Once your usual clients are approved, prompts stop appearing.
|
||||
|
||||
### 12.2 Running as a system signer (e.g. `n_OS_tr`)
|
||||
|
||||
- Configure pre-approvals in the systemd (or equivalent) unit file. One `--preapprove` flag per service.
|
||||
- Verify each pre-approval's caller identity exactly matches what the launcher actually produces. Mismatches cause silent deny (`:no-match` in the log).
|
||||
- Treat the unit file as security-sensitive. Restrict write access to root or its equivalent.
|
||||
- Keep a terminal attached or piped to where prompts can still be seen. Anything not pre-approved still prompts.
|
||||
|
||||
### 12.3 Stopping
|
||||
|
||||
- Press `q` for clean shutdown.
|
||||
- `Ctrl+C` and `SIGTERM` are equivalent — they zeroize and exit.
|
||||
- Closing the terminal is also equivalent. There is no "detach" mode by design.
|
||||
|
||||
### 12.4 Auditing
|
||||
|
||||
- The activity buffer in the running TUI is the session log. It does not survive the process.
|
||||
- For persistent audit, run `n_signer` under a logging supervisor that captures stderr/stdout, knowing the activity buffer reflects post-approval decisions only.
|
||||
|
||||
### 12.5 Rotating
|
||||
|
||||
- "Rotate" in `n_signer` means: quit, change mnemonic source, restart. There is no in-place rotation because there is no persistent state to migrate.
|
||||
|
||||
---
|
||||
|
||||
## 13. Relationship to the current codebase
|
||||
|
||||
All stages from [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) are implemented in the current codebase. This document therefore describes shipped behavior, not a future target state.
|
||||
|
||||
---
|
||||
|
||||
## 14. Things that look like bugs but are security features
|
||||
|
||||
| Symptom | Reason |
|
||||
|---|---|
|
||||
| First request from a new client prompts even though it's the same uid | Deny-by-default. Same-uid trust is gone. Press `[a]` once and you won't see prompts from this client again this session. |
|
||||
| Killing the process loses all approvals and key derivations | Crash-equals-wipe. Restart with the same mnemonic to rebuild deterministically; re-approve clients. |
|
||||
| `purpose_mismatch` when calling `sign_event` against a non-Nostr key | Roles are scoped by purpose. Approvals do not bypass enforcement. |
|
||||
| `unknown_role` for an unregistered `role_path` | `role_path` keys are pre-registration-only by design. Use `nostr_index` for ad-hoc Nostr identities. |
|
||||
| Pre-approval entry doesn't match incoming requests | Caller identity must match exactly — no wildcards. Verify the launcher tags requests the way the unit file expects. |
|
||||
| `role=unknown` in the activity log (current code) | Selector did not resolve. Future versions will prompt to create the identity; current code rejects with `unknown_role` after policy passes. |
|
||||
|
||||
---
|
||||
|
||||
## 15. Quick reference — the security guarantee in one paragraph
|
||||
|
||||
`n_signer` will never sign with a key whose use has not been explicitly approved — either by a human keystroke at the running terminal or by a `--preapprove` flag declared at startup. It will never use a key outside its declared `(purpose, curve)` scope. It will never expose a private key to a client. It will never write key material to disk. Every request is checked twice: once for approval, once for enforcement. Default policy is deny. Approvals vanish on process exit.
|
||||
|
||||
If any of these statements becomes false in code, that is a security bug worth filing immediately.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 16. Post-Quantum Cryptography
|
||||
|
||||
`n_signer` supports three post-quantum algorithms alongside the classical secp256k1, ed25519, and x25519:
|
||||
|
||||
- **ML-DSA-65** (FIPS 204) — lattice-based post-quantum digital signatures
|
||||
- **SLH-DSA-128s** (FIPS 205) — hash-based post-quantum signatures with minimal trust assumptions
|
||||
- **ML-KEM-768** (FIPS 203) — lattice-based post-quantum key encapsulation mechanism
|
||||
|
||||
These are additional options, not replacements for secp256k1. Nostr continues to use secp256k1 exclusively. PQ algorithms are opt-in per role via `purpose="pq-sig"` or `purpose="pq-kem"`. See [`README.md`](../README.md) §4b for the full crypto palette.
|
||||
|
||||
### 16.1 PQ threat model
|
||||
|
||||
The primary PQ threat is **harvest-now-decrypt-later**: an adversary records encrypted traffic or key agreement exchanges today, stores them, and decrypts them once a sufficiently large quantum computer becomes available. ML-KEM-768 addresses this for key agreement — a session key encapsulated with ML-KEM-768 cannot be recovered by a future quantum adversary.
|
||||
|
||||
For signatures, the future risk is **quantum forgery**: a quantum computer could forge classical signatures (ECDSA, Ed25519) given the public key, undermining authentication retroactively. ML-DSA-65 and SLH-DSA-128s address this by providing signatures that resist quantum forgery. The urgency is lower than for key agreement (signatures are forged when needed, not retroactively decrypted), but forward-looking deployments may want PQ signature keys now.
|
||||
|
||||
`n_signer` does not claim to defend against all quantum threats. It provides the PQ primitives; the protocol layer (SSH, TLS, Nostr) must adopt them for the protection to be meaningful.
|
||||
|
||||
### 16.2 Deterministic PQ key derivation
|
||||
|
||||
PQ private keys are not scalars — they are complex mathematical structures (polynomial matrices for lattice schemes, hypertree seeds for hash-based schemes). You cannot use a 32-byte BIP-32 output directly as a PQ private key.
|
||||
|
||||
`n_signer` uses a **non-standard** approach to derive PQ keys deterministically from the mnemonic:
|
||||
|
||||
1. Derive a 32-byte seed from the mnemonic using BIP-32/SLIP-0010 HMAC-SHA512 at a PQ-specific derivation path (e.g. `m/44'/102003'/<n>'/0'/0'` for ML-DSA-65).
|
||||
2. Feed that seed into a SHAKE-256 DRBG (NIST SP 800-90A style).
|
||||
3. Replace PQClean's `randombytes()` callback with this DRBG so keygen is deterministic.
|
||||
4. The PQ algorithm expands the DRBG output into the full key pair.
|
||||
|
||||
**Security argument:**
|
||||
|
||||
- The 32-byte seed from BIP-32 derivation carries full 256 bits of entropy (assuming the mnemonic has full entropy).
|
||||
- SHAKE-256 is a NIST-approved XOF; using it as a DRBG seeded with 256 bits of entropy is sufficient for all three PQ algorithms.
|
||||
- Each role uses a distinct derivation path (distinct coin types 102003/102004/102005), so compromising one role's PQ key does not compromise others.
|
||||
|
||||
**This is non-standard.** There is no NIST or IETF specification for deriving PQ keys from a BIP-39 mnemonic. The approach preserves `n_signer`'s core crash-equals-wipe model: PQ keys are re-derived from the mnemonic on every startup, same as secp256k1. The alternative (random PQ keys with no mnemonic recovery) would break the model.
|
||||
|
||||
**Risk:** If a weakness is found in using DRBG output as PQ keygen randomness, all PQ keys derived this way could be affected. Mitigation: per-role distinct derivation paths limit blast radius. The classical algorithms (secp256k1, ed25519, x25519) are unaffected — they do not use the DRBG.
|
||||
|
||||
Implementation: [`src/pq_drbg.c`](../src/pq_drbg.c), [`src/pq_crypto.c`](../src/pq_crypto.c).
|
||||
|
||||
### 16.3 PQ algorithm maturity
|
||||
|
||||
ML-DSA, SLH-DSA, and ML-KEM are FIPS-standardized (FIPS 203, 204, 205) and have undergone extensive NIST scrutiny. However, they are newer than classical algorithms and have less deployment history. They are provided as **additional options**, not replacements. The enforcement matrix (§5.2) ensures PQ keys cannot be used for Nostr operations and vice versa.
|
||||
|
||||
### 16.4 SLH-DSA-128s signing latency
|
||||
|
||||
SLH-DSA-128s signing on ESP32 can take **5–30 seconds**. This is a UX consideration, not a security issue. The hash-based signature scheme is intentionally compute-bound (that is its security foundation). On the Feather/CYD firmware, the approval prompt should show a "signing..." indicator during the operation.
|
||||
|
||||
The user should choose whether to use SLH-DSA-128s per role. For interactive use where latency matters, ML-DSA-65 is faster. SLH-DSA-128s is appropriate for low-frequency, high-assurance signing where minimal trust assumptions (hash-based, no number-theoretic hardness assumption) are desired.
|
||||
|
||||
### 16.5 Key sizes and memory
|
||||
|
||||
PQ private keys are large compared to classical keys:
|
||||
|
||||
| Algorithm | Private key | Public key | Signature / Ciphertext |
|
||||
|---|---|---|---|
|
||||
| secp256k1 | 32 bytes | 32 bytes | 64 bytes (sig) |
|
||||
| ed25519 | 32 bytes | 32 bytes | 64 bytes (sig) |
|
||||
| ML-DSA-65 | 4032 bytes | 1952 bytes | 3309 bytes (sig) |
|
||||
| SLH-DSA-128s | 64 bytes | 32 bytes | 7856 bytes (sig) |
|
||||
| ML-KEM-768 | 2400 bytes | 1184 bytes | 1088 bytes (ciphertext) |
|
||||
|
||||
With `ROLE_TABLE_MAX_ENTRIES` at 256, a full table of ML-DSA-65 keys would use ~1 MB of `mlock`'d memory (4032 × 256 ≈ 1.03 MB for private keys alone). This is acceptable on host. On ESP32 with 512 KB SRAM, this would not fit — on-demand derivation (deriving a PQ key only when a request targets that role) is the recommended pattern. The existing [`crypto_derive_one`](../src/key_store.c) path already supports this.
|
||||
|
||||
### 16.6 No hybrid signatures yet
|
||||
|
||||
Hybrid signatures (e.g., ed25519 + ML-DSA combined into one signature object) are **future work**. No standard exists for hybrid SSH signatures yet. `n_signer` provides the individual primitives (`sign_data` for ed25519, ML-DSA-65, and SLH-DSA-128s); a hybrid format can be assembled by the client once standards solidify.
|
||||
|
||||
### 16.7 PQ key persistence
|
||||
|
||||
**PQ keys are NOT persisted.** They are re-derived from the mnemonic on every startup, same as secp256k1. There is no PQ key file, no PQ key database, no PQ key cache on disk. Crash-equals-wipe (§10) applies unchanged: if the process dies, all PQ keys are gone and must be re-derived from the mnemonic on next startup.
|
||||
|
||||
This is a deliberate design choice. The deterministic derivation approach (§16.2) makes it possible to recover PQ keys from the mnemonic alone, so persistence would add risk (key material on disk) without adding capability.
|
||||
|
||||
---
|
||||
|
||||
## 17. References
|
||||
|
||||
- [`README.md`](../README.md) — authoritative behavior spec.
|
||||
- [`plans/nsigner.md`](../plans/nsigner.md) — root design plan and decisions log.
|
||||
- [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) — the approval model plan this document tracks.
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md) — wire contract for clients.
|
||||
- [`documents/QUBES_OS.md`](QUBES_OS.md) — Qubes RPC integration.
|
||||
- [`documents/FIPS_DEPLOYMENT.md`](FIPS_DEPLOYMENT.md) — FIPS-mode deployment notes.
|
||||
- [`plans/seed_phrase_uses.md`](../plans/seed_phrase_uses.md) — what one mnemonic can become.
|
||||
- [`plans/post_quantum_crypto.md`](../plans/post_quantum_crypto.md) — post-quantum and multi-algorithm crypto expansion plan.
|
||||
- [`src/policy.c`](../src/policy.c), [`src/server.c`](../src/server.c), [`src/dispatcher.c`](../src/dispatcher.c), [`src/role_table.c`](../src/role_table.c), [`src/selector.c`](../src/selector.c), [`src/enforcement.c`](../src/enforcement.c), [`src/pq_crypto.c`](../src/pq_crypto.c), [`src/pq_drbg.c`](../src/pq_drbg.c) — the security-related code.
|
||||
BIN
examples/__pycache__/cardputer_sign_event.cpython-313.pyc
Normal file
BIN
examples/__pycache__/cardputer_sign_event.cpython-313.pyc
Normal file
Binary file not shown.
BIN
examples/__pycache__/feather_get_public_key.cpython-313.pyc
Normal file
BIN
examples/__pycache__/feather_get_public_key.cpython-313.pyc
Normal file
Binary file not shown.
BIN
examples/__pycache__/kb2040_hidden_signer_client.cpython-313.pyc
Normal file
BIN
examples/__pycache__/kb2040_hidden_signer_client.cpython-313.pyc
Normal file
Binary file not shown.
535
examples/cyd_webserial_demo.html
Normal file
535
examples/cyd_webserial_demo.html
Normal file
@@ -0,0 +1,535 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer CYD Web Serial Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrap { max-width: 980px; margin: 20px auto; padding: 0 14px 24px; }
|
||||
h1 { margin: 0 0 8px; font-size: 1.45rem; }
|
||||
p.note { margin: 0 0 14px; color: var(--muted); }
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-top: 12px; }
|
||||
.card { background: linear-gradient(180deg, var(--panel), var(--panel-2)); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
|
||||
.card h2 { font-size: 1rem; margin: 0 0 10px; }
|
||||
label { font-size: 0.86rem; color: var(--muted); display: block; margin: 6px 0 4px; }
|
||||
input, textarea, select, button { font: inherit; border-radius: 8px; border: 1px solid var(--border); }
|
||||
input, textarea, select { width: 100%; background: #0c131d; color: var(--text); padding: 8px 10px; }
|
||||
textarea { min-height: 64px; resize: vertical; }
|
||||
input[type="number"] { max-width: 130px; }
|
||||
button { background: #1f6feb; color: white; padding: 8px 12px; cursor: pointer; border: 0; }
|
||||
button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.secondary { background: #334155; }
|
||||
.status { padding: 6px 10px; border-radius: 999px; background: #2a3648; color: var(--muted); font-size: 0.85rem; border: 1px solid var(--border); }
|
||||
.status.ok { color: var(--good); border-color: #2f5a3a; }
|
||||
.status.err { color: var(--bad); border-color: #6a3131; }
|
||||
pre { margin: 8px 0 0; background: #0a1018; border: 1px solid #1b2636; color: #d7e2ee; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; font-size: 12px; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.warn { color: #d29922; font-size: 0.8rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer CYD Web Serial Demo</h1>
|
||||
<p class="note">Connect to the CYD (CH340 serial) via Web Serial, then exercise every algorithm and verb in the n_signer API. Chrome/Edge/Brave/Opera only.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect Web Serial</button>
|
||||
<button id="disconnectBtn" class="secondary" disabled>Disconnect</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Get Public Key (algorithm-based) -->
|
||||
<section class="card">
|
||||
<h2>Get Public Key (algorithm)</h2>
|
||||
<label for="gpkAlg">Algorithm</label>
|
||||
<select id="gpkAlg">
|
||||
<option>secp256k1</option><option>ed25519</option><option>x25519</option>
|
||||
<option>ml-dsa-65</option><option>slh-dsa-128s</option><option>ml-kem-768</option>
|
||||
</select>
|
||||
<label for="gpkIdx">Index</label>
|
||||
<input id="gpkIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="gpkBtn" disabled>get_public_key</button>
|
||||
</div>
|
||||
<pre id="gpkOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Get Public Key -->
|
||||
<section class="card">
|
||||
<h2>nostr_get_public_key</h2>
|
||||
<label for="ngpkIdx">nostr_index</label>
|
||||
<input id="ngpkIdx" type="number" value="0" min="0" />
|
||||
<label for="ngpkFmt">format</label>
|
||||
<select id="ngpkFmt"><option>bare</option><option>structured</option></select>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="ngpkBtn" disabled>nostr_get_public_key</button>
|
||||
</div>
|
||||
<pre id="ngpkOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Sign / Verify -->
|
||||
<section class="card">
|
||||
<h2>sign / verify</h2>
|
||||
<label for="signAlg">Algorithm</label>
|
||||
<select id="signAlg">
|
||||
<option>secp256k1</option><option>ed25519</option><option>ml-dsa-65</option><option>slh-dsa-128s</option>
|
||||
</select>
|
||||
<label for="signIdx">Index</label>
|
||||
<input id="signIdx" type="number" value="0" min="0" />
|
||||
<label for="signScheme">scheme (secp256k1 only)</label>
|
||||
<select id="signScheme"><option>schnorr</option><option>ecdsa</option></select>
|
||||
<label for="signMsg">message (hex)</label>
|
||||
<input id="signMsg" value="68656c6c6f" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signBtn" disabled>sign</button>
|
||||
<button id="verifyBtn" disabled>verify</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- KEM encapsulate / decapsulate -->
|
||||
<section class="card">
|
||||
<h2>encapsulate / decapsulate (ml-kem-768)</h2>
|
||||
<label for="kemPeer">peer pubkey hex (1184 bytes / 2368 hex) — leave empty to use self pubkey</label>
|
||||
<textarea id="kemPeer" placeholder="auto: uses ml-kem-768 get_public_key"></textarea>
|
||||
<label for="kemIdx">index (for decapsulate)</label>
|
||||
<input id="kemIdx" type="number" value="0" min="0" />
|
||||
<label for="kemCt">ciphertext hex (for decapsulate)</label>
|
||||
<textarea id="kemCt" placeholder="filled by encapsulate"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="encapBtn" disabled>encapsulate</button>
|
||||
<button id="decapBtn" disabled>decapsulate</button>
|
||||
</div>
|
||||
<pre id="kemOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- derive_shared_secret (x25519) -->
|
||||
<section class="card">
|
||||
<h2>derive_shared_secret (x25519)</h2>
|
||||
<label for="x25519Peer">peer pubkey hex (32 bytes / 64 hex)</label>
|
||||
<input id="x25519Peer" placeholder="64 hex chars" />
|
||||
<label for="x25519Idx">index</label>
|
||||
<input id="x25519Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="x25519Btn" disabled>derive_shared_secret</button>
|
||||
</div>
|
||||
<pre id="x25519Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- derive (HMAC) -->
|
||||
<section class="card">
|
||||
<h2>derive (secp256k1 HMAC-SHA256)</h2>
|
||||
<label for="deriveData">data (UTF-8 string)</label>
|
||||
<input id="deriveData" value="hello-derive" />
|
||||
<label for="deriveIdx">index (required)</label>
|
||||
<input id="deriveIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="deriveBtn" disabled>derive</button>
|
||||
</div>
|
||||
<pre id="deriveOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Sign Event -->
|
||||
<section class="card">
|
||||
<h2>nostr_sign_event</h2>
|
||||
<label for="nseContent">content</label>
|
||||
<textarea id="nseContent">hello from cyd webserial demo</textarea>
|
||||
<label for="nseIdx">nostr_index</label>
|
||||
<input id="nseIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nseBtn" disabled>nostr_sign_event</button>
|
||||
</div>
|
||||
<pre id="nseOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Mine Event -->
|
||||
<section class="card">
|
||||
<h2>nostr_mine_event</h2>
|
||||
<p class="warn">Slow on ESP32 — uses single-threaded PoW. Keep difficulty low.</p>
|
||||
<label for="nmeContent">content</label>
|
||||
<textarea id="nmeContent">mined by cyd</textarea>
|
||||
<label for="nmeIdx">nostr_index</label>
|
||||
<input id="nmeIdx" type="number" value="0" min="0" />
|
||||
<label for="nmeDiff">difficulty (leading zero bits)</label>
|
||||
<input id="nmeDiff" type="number" value="4" min="1" max="16" />
|
||||
<label for="nmeTimeout">timeout (sec)</label>
|
||||
<input id="nmeTimeout" type="number" value="30" min="1" max="60" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nmeBtn" disabled>nostr_mine_event</button>
|
||||
</div>
|
||||
<pre id="nmeOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- NIP-04 -->
|
||||
<section class="card">
|
||||
<h2>nostr_nip04_encrypt / decrypt</h2>
|
||||
<label for="nip04Peer">peer pubkey hex (32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="64 hex chars" />
|
||||
<label for="nip04Msg">plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<label for="nip04Cipher">ciphertext (for decrypt)</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<label for="nip04Idx">nostr_index</label>
|
||||
<input id="nip04Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>encrypt</button>
|
||||
<button id="nip04DecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- NIP-44 -->
|
||||
<section class="card">
|
||||
<h2>nostr_nip44_encrypt / decrypt</h2>
|
||||
<label for="nip44Peer">peer pubkey hex (32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="64 hex chars" />
|
||||
<label for="nip44Msg">plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<label for="nip44Cipher">ciphertext (for decrypt)</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<label for="nip44Idx">nostr_index</label>
|
||||
<input id="nip44Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>encrypt</button>
|
||||
<button id="nip44DecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- OTP encrypt / decrypt -->
|
||||
<section class="card">
|
||||
<h2>encrypt / decrypt (otp)</h2>
|
||||
<p class="note">OTP pad is derived from the mnemonic on the CYD. Offset advances monotonically.</p>
|
||||
<label for="otpPlain">plaintext (base64)</label>
|
||||
<textarea id="otpPlain">SGVsbG8sIE9UUCB3b3JsZCE=</textarea>
|
||||
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
|
||||
<textarea id="otpCipher" placeholder="filled by encrypt"></textarea>
|
||||
<label for="otpEnc">encoding</label>
|
||||
<select id="otpEnc"><option>ascii</option><option>binary</option></select>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="otpEncBtn" disabled>encrypt</button>
|
||||
<button id="otpDecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="otpOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const disconnectBtn = document.getElementById("disconnectBtn");
|
||||
|
||||
let port = null;
|
||||
let reader = null;
|
||||
let writer = null;
|
||||
let readLoopRunning = false;
|
||||
let rxBuffer = new Uint8Array(0);
|
||||
let pendingResolve = null;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function utf8(s) { return new TextEncoder().encode(s); }
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
function pretty(value) {
|
||||
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
const content = "cyd-webserial-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
return { id, pubkey: callerPubX, created_at: createdAt, kind: 27235, tags, content, sig: sigHex };
|
||||
}
|
||||
|
||||
/* ---- Web Serial transport ---- */
|
||||
async function readLoop() {
|
||||
readLoopRunning = true;
|
||||
while (readLoopRunning && reader) {
|
||||
try {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
const next = new Uint8Array(rxBuffer.length + value.length);
|
||||
next.set(rxBuffer, 0);
|
||||
next.set(value, rxBuffer.length);
|
||||
rxBuffer = next;
|
||||
tryDeliver();
|
||||
}
|
||||
} catch (e) {
|
||||
log("read error:", e.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
readLoopRunning = false;
|
||||
}
|
||||
|
||||
function tryDeliver() {
|
||||
if (pendingResolve === null) return;
|
||||
while (rxBuffer.length >= 4) {
|
||||
const len = (rxBuffer[0] << 24) | (rxBuffer[1] << 16) | (rxBuffer[2] << 8) | rxBuffer[3];
|
||||
if (len <= 0 || len > 16384) {
|
||||
/* resync: drop one byte */
|
||||
rxBuffer = rxBuffer.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (rxBuffer.length < 4 + len) return;
|
||||
const payload = rxBuffer.slice(4, 4 + len);
|
||||
rxBuffer = rxBuffer.slice(4 + len);
|
||||
const text = new TextDecoder().decode(payload);
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
||||
const r = pendingResolve;
|
||||
pendingResolve = null;
|
||||
r(parsed);
|
||||
if (pendingResolve === null) return;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
if (!writer) throw new Error("not connected");
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await writer.write(frame);
|
||||
const resp = await new Promise((resolve, reject) => {
|
||||
pendingResolve = resolve;
|
||||
setTimeout(() => {
|
||||
if (pendingResolve === resolve) {
|
||||
pendingResolve = null;
|
||||
reject(new Error("timeout (30s) — check the CYD screen for an approval prompt"));
|
||||
}
|
||||
}, 65000);
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
async function callVerb(method, params, outEl) {
|
||||
outEl.textContent = "→ " + method + " " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { id: String(Math.floor(Math.random() * 1e9)), method, params, auth };
|
||||
const resp = await sendRpc(req);
|
||||
outEl.textContent += "\n← " + pretty(resp);
|
||||
} catch (e) {
|
||||
outEl.textContent += "\n✗ " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Connect / Disconnect ---- */
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate: 115200 });
|
||||
reader = port.readable.getReader();
|
||||
writer = port.writable.getWriter();
|
||||
rxBuffer = new Uint8Array(0);
|
||||
readLoop();
|
||||
setStatus("Connected", "ok");
|
||||
log("Connected to CYD via Web Serial @ 115200 baud");
|
||||
document.querySelectorAll("button[id$='Btn']").forEach(b => {
|
||||
if (b !== connectBtn && b !== disconnectBtn) b.disabled = false;
|
||||
});
|
||||
disconnectBtn.disabled = false;
|
||||
connectBtn.disabled = true;
|
||||
} catch (e) {
|
||||
setStatus("Error", "err");
|
||||
log("connect failed:", e.message);
|
||||
}
|
||||
});
|
||||
|
||||
disconnectBtn.addEventListener("click", async () => {
|
||||
readLoopRunning = false;
|
||||
try { if (reader) await reader.cancel(); } catch {}
|
||||
try { if (writer) await writer.releaseLock(); } catch {}
|
||||
try { if (port) await port.close(); } catch {}
|
||||
reader = null; writer = null; port = null;
|
||||
setStatus("Disconnected");
|
||||
document.querySelectorAll("button[id$='Btn']").forEach(b => {
|
||||
if (b !== connectBtn) b.disabled = true;
|
||||
});
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
log("Disconnected");
|
||||
});
|
||||
|
||||
/* ---- Verb wiring ---- */
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
$("gpkBtn").addEventListener("click", () => {
|
||||
const alg = $("gpkAlg").value, idx = Number($("gpkIdx").value || 0);
|
||||
callVerb("get_public_key", [{ algorithm: alg, index: idx }], $("gpkOut"));
|
||||
});
|
||||
$("ngpkBtn").addEventListener("click", () => {
|
||||
const idx = Number($("ngpkIdx").value || 0), fmt = $("ngpkFmt").value;
|
||||
const opts = { nostr_index: idx };
|
||||
if (fmt === "structured") opts.format = "structured";
|
||||
callVerb("nostr_get_public_key", [opts], $("ngpkOut"));
|
||||
});
|
||||
|
||||
$("signBtn").addEventListener("click", () => {
|
||||
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
|
||||
const scheme = $("signScheme").value, msg = $("signMsg").value;
|
||||
const opts = { algorithm: alg, index: idx };
|
||||
if (alg === "secp256k1") opts.scheme = scheme;
|
||||
callVerb("sign", [msg, opts], $("signOut"));
|
||||
});
|
||||
$("verifyBtn").addEventListener("click", async () => {
|
||||
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
|
||||
const scheme = $("signScheme").value, msg = $("signMsg").value;
|
||||
const opts = { algorithm: alg, index: idx };
|
||||
if (alg === "secp256k1") opts.scheme = scheme;
|
||||
/* parse the last sign result to get the signature */
|
||||
const outText = $("signOut").textContent;
|
||||
const m = outText.match(/"signature"\s*:\s*"([0-9a-f]+)"/);
|
||||
if (!m) { $("signOut").textContent += "\n✗ no signature found — run sign first"; return; }
|
||||
callVerb("verify", [msg, m[1], opts], $("signOut"));
|
||||
});
|
||||
|
||||
$("encapBtn").addEventListener("click", async () => {
|
||||
let peer = $("kemPeer").value.trim();
|
||||
if (!peer) {
|
||||
/* fetch self ml-kem-768 pubkey first */
|
||||
const opts = [{ algorithm: "ml-kem-768", index: Number($("kemIdx").value || 0) }];
|
||||
const auth = await buildAuth("get_public_key", opts);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "get_public_key", params: opts, auth });
|
||||
peer = resp.result && JSON.parse(resp.result).public_key;
|
||||
if (!peer) { $("kemOut").textContent = "✗ could not fetch self pubkey"; return; }
|
||||
$("kemPeer").value = peer;
|
||||
}
|
||||
callVerb("encapsulate", [peer, { algorithm: "ml-kem-768" }], $("kemOut"));
|
||||
});
|
||||
$("decapBtn").addEventListener("click", () => {
|
||||
const ct = $("kemCt").value.trim();
|
||||
const idx = Number($("kemIdx").value || 0);
|
||||
if (!ct) { $("kemOut").textContent = "✗ paste a ciphertext first (from encapsulate)"; return; }
|
||||
callVerb("decapsulate", [ct, { algorithm: "ml-kem-768", index: idx }], $("kemOut"));
|
||||
});
|
||||
|
||||
$("x25519Btn").addEventListener("click", () => {
|
||||
const peer = $("x25519Peer").value.trim();
|
||||
const idx = Number($("x25519Idx").value || 0);
|
||||
if (!peer) { $("x25519Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("derive_shared_secret", [peer, { algorithm: "x25519", index: idx }], $("x25519Out"));
|
||||
});
|
||||
|
||||
$("deriveBtn").addEventListener("click", () => {
|
||||
const data = $("deriveData").value;
|
||||
const idx = Number($("deriveIdx").value || 0);
|
||||
callVerb("derive", [data, { algorithm: "secp256k1", index: idx }], $("deriveOut"));
|
||||
});
|
||||
|
||||
$("nseBtn").addEventListener("click", () => {
|
||||
const content = $("nseContent").value;
|
||||
const idx = Number($("nseIdx").value || 0);
|
||||
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
|
||||
callVerb("nostr_sign_event", [event, { nostr_index: idx }], $("nseOut"));
|
||||
});
|
||||
|
||||
$("nmeBtn").addEventListener("click", () => {
|
||||
const content = $("nmeContent").value;
|
||||
const idx = Number($("nmeIdx").value || 0);
|
||||
const diff = Number($("nmeDiff").value || 4);
|
||||
const timeout = Number($("nmeTimeout").value || 30);
|
||||
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
|
||||
callVerb("nostr_mine_event", [event, { nostr_index: idx, difficulty: diff, timeout_sec: timeout }], $("nmeOut"));
|
||||
});
|
||||
|
||||
const nip04Enc = () => {
|
||||
const peer = $("nip04Peer").value.trim(), msg = $("nip04Msg").value, idx = Number($("nip04Idx").value || 0);
|
||||
if (!peer) { $("nip04Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip04_encrypt", [peer, msg, { nostr_index: idx }], $("nip04Out"));
|
||||
};
|
||||
const nip04Dec = () => {
|
||||
const peer = $("nip04Peer").value.trim(), ct = $("nip04Cipher").value, idx = Number($("nip04Idx").value || 0);
|
||||
if (!peer || !ct) { $("nip04Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
|
||||
callVerb("nostr_nip04_decrypt", [peer, ct, { nostr_index: idx }], $("nip04Out"));
|
||||
};
|
||||
$("nip04EncBtn").addEventListener("click", nip04Enc);
|
||||
$("nip04DecBtn").addEventListener("click", nip04Dec);
|
||||
|
||||
const nip44Enc = () => {
|
||||
const peer = $("nip44Peer").value.trim(), msg = $("nip44Msg").value, idx = Number($("nip44Idx").value || 0);
|
||||
if (!peer) { $("nip44Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip44_encrypt", [peer, msg, { nostr_index: idx }], $("nip44Out"));
|
||||
};
|
||||
const nip44Dec = () => {
|
||||
const peer = $("nip44Peer").value.trim(), ct = $("nip44Cipher").value, idx = Number($("nip44Idx").value || 0);
|
||||
if (!peer || !ct) { $("nip44Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
|
||||
callVerb("nostr_nip44_decrypt", [peer, ct, { nostr_index: idx }], $("nip44Out"));
|
||||
};
|
||||
$("nip44EncBtn").addEventListener("click", nip44Enc);
|
||||
$("nip44DecBtn").addEventListener("click", nip44Dec);
|
||||
|
||||
$("otpEncBtn").addEventListener("click", () => {
|
||||
const pt = $("otpPlain").value, enc = $("otpEnc").value;
|
||||
callVerb("encrypt", [pt, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
});
|
||||
$("otpDecBtn").addEventListener("click", () => {
|
||||
const ct = $("otpCipher").value, enc = $("otpEnc").value;
|
||||
if (!ct) { $("otpOut").textContent = "✗ paste ciphertext first (from encrypt)"; return; }
|
||||
callVerb("decrypt", [ct, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
});
|
||||
|
||||
if (!("serial" in navigator)) {
|
||||
log("Web Serial not supported in this browser. Use Chrome/Edge/Brave/Opera.");
|
||||
connectBtn.disabled = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
136
examples/feather_get_public_key.py
Normal file
136
examples/feather_get_public_key.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from coincurve import PrivateKey
|
||||
|
||||
|
||||
def read_exact(ser: serial.Serial, n: int, timeout_s: float = 8.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
out = bytearray()
|
||||
while len(out) < n and time.time() < end:
|
||||
chunk = ser.read(n - len(out))
|
||||
if chunk:
|
||||
out.extend(chunk)
|
||||
continue
|
||||
time.sleep(0.005)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def recv_frame(ser: serial.Serial, timeout_s: float = 10.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
window = bytearray()
|
||||
|
||||
while time.time() < end:
|
||||
b = ser.read(1)
|
||||
if not b:
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
window.extend(b)
|
||||
if len(window) > 4:
|
||||
window.pop(0)
|
||||
if len(window) < 4:
|
||||
continue
|
||||
(n,) = struct.unpack(">I", bytes(window))
|
||||
# Keep this cap small to avoid getting stuck on false headers in stream noise.
|
||||
if n == 0 or n > 64 * 1024:
|
||||
continue
|
||||
body = read_exact(ser, n, timeout_s=max(0.2, end - time.time()))
|
||||
if len(body) != n:
|
||||
continue
|
||||
if body[:1] not in (b"{", b"["):
|
||||
continue
|
||||
try:
|
||||
json.loads(body.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
continue
|
||||
return body
|
||||
|
||||
return b""
|
||||
|
||||
|
||||
def build_auth_envelope(method: str, params, caller_priv: bytes, label: str = "py-cli") -> dict:
|
||||
body_json = json.dumps(params, separators=(",", ":")).encode("utf-8")
|
||||
body_hash = hashlib.sha256(body_json).hexdigest()
|
||||
|
||||
sk = PrivateKey(caller_priv)
|
||||
caller_pub_x = sk.public_key.format(compressed=True)[1:].hex()
|
||||
|
||||
created_at = int(time.time())
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
serialized = json.dumps(
|
||||
[0, caller_pub_x, created_at, 27235, tags, label],
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
return {
|
||||
"id": event_id,
|
||||
"pubkey": caller_pub_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": label,
|
||||
"sig": sig,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"
|
||||
|
||||
caller_priv = bytes(range(1, 33))
|
||||
params = []
|
||||
|
||||
req = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": params,
|
||||
"auth": build_auth_envelope("get_public_key", params, caller_priv),
|
||||
}
|
||||
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(body)) + body
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = 115200
|
||||
ser.timeout = 0.2
|
||||
ser.rtscts = False
|
||||
ser.dsrdtr = False
|
||||
# Set modem-control lines before open to avoid auto-reset pulses.
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
|
||||
with ser:
|
||||
# Let the board settle if OS still caused a reconnect.
|
||||
time.sleep(3.0)
|
||||
|
||||
for attempt in range(1, 4):
|
||||
ser.reset_input_buffer()
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
resp_body = recv_frame(ser, timeout_s=10.0)
|
||||
if resp_body:
|
||||
print(resp_body.decode("utf-8", errors="replace"))
|
||||
return 0
|
||||
|
||||
print(f"No framed response received (attempt {attempt}/3)")
|
||||
time.sleep(0.5)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
199
examples/feather_sign_event.py
Normal file
199
examples/feather_sign_event.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from coincurve import PrivateKey, PublicKeyXOnly
|
||||
|
||||
|
||||
def read_exact(ser: serial.Serial, n: int, timeout_s: float = 8.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
out = bytearray()
|
||||
while len(out) < n and time.time() < end:
|
||||
chunk = ser.read(n - len(out))
|
||||
if chunk:
|
||||
out.extend(chunk)
|
||||
continue
|
||||
time.sleep(0.005)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def recv_frame(ser: serial.Serial, timeout_s: float = 10.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
window = bytearray()
|
||||
|
||||
while time.time() < end:
|
||||
b = ser.read(1)
|
||||
if not b:
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
|
||||
window.extend(b)
|
||||
if len(window) > 4:
|
||||
window.pop(0)
|
||||
|
||||
if len(window) < 4:
|
||||
continue
|
||||
|
||||
(n,) = struct.unpack(">I", bytes(window))
|
||||
if n == 0 or n > 1_000_000:
|
||||
continue
|
||||
|
||||
body = read_exact(ser, n, timeout_s=max(0.2, end - time.time()))
|
||||
if len(body) != n:
|
||||
return b""
|
||||
if body[:1] not in (b"{", b"["):
|
||||
continue
|
||||
|
||||
try:
|
||||
json.loads(body.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
# Probably matched a false header inside stream noise; keep scanning.
|
||||
continue
|
||||
|
||||
return body
|
||||
|
||||
return b""
|
||||
|
||||
|
||||
def nostr_serialize(pubkey: str, created_at: int, kind: int, tags: list, content: str) -> bytes:
|
||||
arr = [0, pubkey, created_at, kind, tags, content]
|
||||
return json.dumps(arr, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
def event_id_hex(event_obj: dict) -> str:
|
||||
ser = nostr_serialize(
|
||||
event_obj["pubkey"],
|
||||
int(event_obj["created_at"]),
|
||||
int(event_obj["kind"]),
|
||||
event_obj["tags"],
|
||||
event_obj["content"],
|
||||
)
|
||||
return hashlib.sha256(ser).hexdigest()
|
||||
|
||||
|
||||
def verify_event(event_obj: dict) -> bool:
|
||||
ev_id = event_id_hex(event_obj)
|
||||
if ev_id != event_obj.get("id"):
|
||||
return False
|
||||
|
||||
pk = PublicKeyXOnly(bytes.fromhex(event_obj["pubkey"]))
|
||||
sig = bytes.fromhex(event_obj["sig"])
|
||||
return pk.verify(sig, bytes.fromhex(ev_id))
|
||||
|
||||
|
||||
def build_auth_envelope(method: str, params, caller_priv: bytes, label: str = "py-cli") -> dict:
|
||||
body_json = json.dumps(params, separators=(",", ":")).encode("utf-8")
|
||||
body_hash = hashlib.sha256(body_json).hexdigest()
|
||||
|
||||
sk = PrivateKey(caller_priv)
|
||||
caller_pub_x = sk.public_key.format(compressed=True)[1:].hex()
|
||||
created_at = int(time.time())
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
serialized = json.dumps(
|
||||
[0, caller_pub_x, created_at, 27235, tags, label],
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
return {
|
||||
"id": event_id,
|
||||
"pubkey": caller_pub_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": label,
|
||||
"sig": sig,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = [a for a in sys.argv[1:]]
|
||||
no_auth = False
|
||||
if "--no-auth" in args:
|
||||
no_auth = True
|
||||
args.remove("--no-auth")
|
||||
use_alt_caller = "--alt-caller" in args
|
||||
if use_alt_caller:
|
||||
args.remove("--alt-caller")
|
||||
port = args[0] if args else "/dev/ttyACM0"
|
||||
|
||||
caller_priv = bytes(range(1, 33)) if not use_alt_caller else bytes(range(33, 65))
|
||||
|
||||
unsigned_event = {
|
||||
"kind": 1,
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"content": "hello from feather phase4",
|
||||
}
|
||||
|
||||
params = [unsigned_event]
|
||||
req = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "nostr_sign_event",
|
||||
"params": params,
|
||||
}
|
||||
if not no_auth:
|
||||
req["auth"] = build_auth_envelope("nostr_sign_event", params, caller_priv)
|
||||
|
||||
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(body)) + body
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = 115200
|
||||
ser.timeout = 0.2
|
||||
ser.rtscts = False
|
||||
ser.dsrdtr = False
|
||||
# Set modem-control lines before open to avoid auto-reset pulses.
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
|
||||
with ser:
|
||||
# Let board fully settle after USB open/reconnect.
|
||||
time.sleep(3.0)
|
||||
ser.reset_input_buffer()
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
resp_raw = recv_frame(ser, timeout_s=45.0)
|
||||
if not resp_raw:
|
||||
print("No framed response received")
|
||||
return 1
|
||||
|
||||
try:
|
||||
resp = json.loads(resp_raw.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
print("Invalid JSON response bytes:", resp_raw)
|
||||
return 1
|
||||
if "error" in resp:
|
||||
print("RPC error:", json.dumps(resp["error"], separators=(",", ":")))
|
||||
return 1
|
||||
|
||||
result = resp.get("result")
|
||||
if not isinstance(result, dict):
|
||||
print("Invalid RPC result")
|
||||
return 1
|
||||
|
||||
ok = verify_event(result)
|
||||
print(json.dumps(result, separators=(",", ":")))
|
||||
if not ok:
|
||||
print("signature invalid")
|
||||
return 1
|
||||
|
||||
print("✓ signature valid")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
527
examples/feather_webusb_demo.html
Normal file
527
examples/feather_webusb_demo.html
Normal file
@@ -0,0 +1,527 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer Feather WebUSB Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 980px;
|
||||
margin: 20px auto;
|
||||
padding: 0 14px 24px;
|
||||
}
|
||||
h1 { margin: 0 0 8px; font-size: 1.45rem; }
|
||||
p.note { margin: 0 0 14px; color: var(--muted); }
|
||||
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.86rem;
|
||||
color: var(--muted);
|
||||
display: block;
|
||||
margin: 6px 0 4px;
|
||||
}
|
||||
input, textarea, button {
|
||||
font: inherit;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
background: #0c131d;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
textarea { min-height: 84px; resize: vertical; }
|
||||
input[type="number"] { max-width: 130px; }
|
||||
|
||||
button {
|
||||
background: #1f6feb;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
}
|
||||
button[disabled] {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.secondary { background: #334155; }
|
||||
|
||||
.status {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #2a3648;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.status.ok { color: var(--good); border-color: #2f5a3a; }
|
||||
.status.err { color: var(--bad); border-color: #6a3131; }
|
||||
|
||||
pre {
|
||||
margin: 8px 0 0;
|
||||
background: #0a1018;
|
||||
border: 1px solid #1b2636;
|
||||
color: #d7e2ee;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
max-height: 220px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer Feather WebUSB Demo</h1>
|
||||
<p class="note">Connect to the board, choose a key index, fetch a pubkey, sign a kind 1 event, and test NIP-04 / NIP-44 encrypt + decrypt RPCs.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect WebUSB</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Public Key</h2>
|
||||
<label for="keyIndex">Key index (nostr_index)</label>
|
||||
<input id="keyIndex" type="text" inputmode="numeric" pattern="[0-9]*" value="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="pubkeyBtn" disabled>Get Public Key</button>
|
||||
</div>
|
||||
<pre id="pubkeyOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sign Kind 1 Event</h2>
|
||||
<label for="kind1Content">Content</label>
|
||||
<textarea id="kind1Content">hello from feather webusb demo</textarea>
|
||||
<label for="kind1Tags">Tags JSON (array)</label>
|
||||
<input id="kind1Tags" value="[]" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signKind1Btn" disabled>Create + Sign kind 1</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Encrypt</h2>
|
||||
<label for="nip04Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Msg">Plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>Encrypt (nip04_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Decrypt</h2>
|
||||
<label for="nip04DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Cipher">Ciphertext</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04DecBtn" disabled>Decrypt (nip04_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04DecOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Encrypt</h2>
|
||||
<label for="nip44Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Msg">Plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>Encrypt (nip44_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Decrypt</h2>
|
||||
<label for="nip44DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Cipher">Ciphertext</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44DecBtn" disabled>Decrypt (nip44_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44DecOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const pubkeyBtn = document.getElementById("pubkeyBtn");
|
||||
const signKind1Btn = document.getElementById("signKind1Btn");
|
||||
const nip04EncBtn = document.getElementById("nip04EncBtn");
|
||||
const nip04DecBtn = document.getElementById("nip04DecBtn");
|
||||
const nip44EncBtn = document.getElementById("nip44EncBtn");
|
||||
const nip44DecBtn = document.getElementById("nip44DecBtn");
|
||||
|
||||
const keyIndexEl = document.getElementById("keyIndex");
|
||||
const pubkeyOutEl = document.getElementById("pubkeyOut");
|
||||
|
||||
const kind1ContentEl = document.getElementById("kind1Content");
|
||||
const kind1TagsEl = document.getElementById("kind1Tags");
|
||||
const signOutEl = document.getElementById("signOut");
|
||||
|
||||
const nip04PeerEl = document.getElementById("nip04Peer");
|
||||
const nip04MsgEl = document.getElementById("nip04Msg");
|
||||
const nip04OutEl = document.getElementById("nip04Out");
|
||||
const nip04DecPeerEl = document.getElementById("nip04DecPeer");
|
||||
const nip04CipherEl = document.getElementById("nip04Cipher");
|
||||
const nip04DecOutEl = document.getElementById("nip04DecOut");
|
||||
|
||||
const nip44PeerEl = document.getElementById("nip44Peer");
|
||||
const nip44MsgEl = document.getElementById("nip44Msg");
|
||||
const nip44OutEl = document.getElementById("nip44Out");
|
||||
const nip44DecPeerEl = document.getElementById("nip44DecPeer");
|
||||
const nip44CipherEl = document.getElementById("nip44Cipher");
|
||||
const nip44DecOutEl = document.getElementById("nip44DecOut");
|
||||
|
||||
let dev = null;
|
||||
let iface = null;
|
||||
let ownPubkey = "";
|
||||
const EP_OUT = 1;
|
||||
const EP_IN = 1;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function utf8(s) {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
|
||||
function getIndexOptions() {
|
||||
const raw = Number.parseInt(String(keyIndexEl.value ?? "0"), 10);
|
||||
const index = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
||||
return { nostr_index: index };
|
||||
}
|
||||
|
||||
function pretty(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
// Demo caller key only (not secret in browser demo).
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
|
||||
const content = "webusb-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
|
||||
return {
|
||||
id,
|
||||
pubkey: callerPubX,
|
||||
created_at: createdAt,
|
||||
kind: 27235,
|
||||
tags,
|
||||
content,
|
||||
sig: sigHex,
|
||||
};
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await dev.transferOut(EP_OUT, frame);
|
||||
|
||||
const deadline = Date.now() + 10000;
|
||||
let ring = new Uint8Array(0);
|
||||
while (Date.now() < deadline) {
|
||||
const r = await dev.transferIn(EP_IN, 512);
|
||||
if (!r.data || r.data.byteLength === 0) continue;
|
||||
|
||||
const chunk = new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength);
|
||||
const next = new Uint8Array(ring.length + chunk.length);
|
||||
next.set(ring, 0);
|
||||
next.set(chunk, ring.length);
|
||||
ring = next;
|
||||
|
||||
while (ring.length >= 4) {
|
||||
const n = (ring[0] << 24) | (ring[1] << 16) | (ring[2] << 8) | ring[3];
|
||||
if (n <= 0 || n > 1_000_000) {
|
||||
ring = ring.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (ring.length < 4 + n) break;
|
||||
|
||||
const payload = ring.slice(4, 4 + n);
|
||||
ring = ring.slice(4 + n);
|
||||
const txt = new TextDecoder().decode(payload);
|
||||
return JSON.parse(txt);
|
||||
}
|
||||
}
|
||||
throw new Error("Timed out waiting for framed response");
|
||||
}
|
||||
|
||||
async function rpcCall(method, params, id = "web-1") {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { jsonrpc: "2.0", id, method, params, auth };
|
||||
log("→", method, JSON.stringify(params));
|
||||
const resp = await sendRpc(req);
|
||||
log("←", method, JSON.stringify(resp));
|
||||
return resp;
|
||||
}
|
||||
|
||||
function requirePeerHex(peer) {
|
||||
const v = String(peer || "").trim().toLowerCase();
|
||||
if (!/^[0-9a-f]{64}$/.test(v)) {
|
||||
throw new Error("Peer pubkey must be exactly 64 hex chars (x-only pubkey)");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function requireStringResult(resp, what) {
|
||||
if (resp && typeof resp.result === "string") {
|
||||
return resp.result;
|
||||
}
|
||||
throw new Error(`${what} failed: ${pretty(resp)}`);
|
||||
}
|
||||
|
||||
async function fetchOwnPubkeyAndFillPeers() {
|
||||
const params = [getIndexOptions()];
|
||||
const resp = await rpcCall("get_public_key", params, "web-own-pubkey");
|
||||
if (resp && typeof resp.result === "string") {
|
||||
ownPubkey = resp.result.trim().toLowerCase();
|
||||
pubkeyOutEl.textContent = pretty(resp);
|
||||
if (/^[0-9a-f]{64}$/.test(ownPubkey)) {
|
||||
nip04PeerEl.value = ownPubkey;
|
||||
nip04DecPeerEl.value = ownPubkey;
|
||||
nip44PeerEl.value = ownPubkey;
|
||||
nip44DecPeerEl.value = ownPubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
dev = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
|
||||
await dev.open();
|
||||
if (dev.configuration === null) {
|
||||
await dev.selectConfiguration(1);
|
||||
}
|
||||
|
||||
const intf = dev.configuration.interfaces.find(i =>
|
||||
i.alternates.some(a => a.interfaceClass === 0xff)
|
||||
);
|
||||
if (!intf) throw new Error("No vendor WebUSB interface found");
|
||||
|
||||
iface = intf.interfaceNumber;
|
||||
await dev.claimInterface(iface);
|
||||
const alt = intf.alternates.find(a => a.interfaceClass === 0xff);
|
||||
await dev.selectAlternateInterface(iface, alt.alternateSetting);
|
||||
|
||||
await dev.controlTransferOut({
|
||||
requestType: "class",
|
||||
recipient: "interface",
|
||||
request: 0x22,
|
||||
value: 1,
|
||||
index: iface,
|
||||
});
|
||||
|
||||
pubkeyBtn.disabled = false;
|
||||
signKind1Btn.disabled = false;
|
||||
nip04EncBtn.disabled = false;
|
||||
nip04DecBtn.disabled = false;
|
||||
nip44EncBtn.disabled = false;
|
||||
nip44DecBtn.disabled = false;
|
||||
setStatus(`Connected (iface ${iface})`, "ok");
|
||||
log("Connected. Interface", String(iface));
|
||||
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
log("Default peer pubkeys set to selected signer pubkey");
|
||||
} catch (e) {
|
||||
log("Auto pubkey fetch failed:", String(e));
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
setStatus("Connect failed", "err");
|
||||
log("Connect failed:", String(e));
|
||||
}
|
||||
});
|
||||
|
||||
pubkeyBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
} catch (e) {
|
||||
pubkeyOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
signKind1Btn.addEventListener("click", async () => {
|
||||
try {
|
||||
let tags = [];
|
||||
try {
|
||||
tags = JSON.parse(kind1TagsEl.value || "[]");
|
||||
if (!Array.isArray(tags)) throw new Error("tags must be an array");
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid tags JSON: ${String(e)}`);
|
||||
}
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: String(kind1ContentEl.value || ""),
|
||||
};
|
||||
|
||||
const params = [unsignedEvent, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_sign_event", params, "web-sign-kind1");
|
||||
signOutEl.textContent = pretty(resp?.result ?? resp);
|
||||
} catch (e) {
|
||||
signOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04PeerEl.value);
|
||||
const msg = String(nip04MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip04_encrypt", params, "web-nip04-enc");
|
||||
nip04OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip04DecPeerEl.value = peer;
|
||||
nip04CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip04OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04DecPeerEl.value);
|
||||
const ciphertext = String(nip04CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip04_decrypt", params, "web-nip04-dec");
|
||||
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
|
||||
} catch (e) {
|
||||
nip04DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44PeerEl.value);
|
||||
const msg = String(nip44MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip44_encrypt", params, "web-nip44-enc");
|
||||
nip44OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip44DecPeerEl.value = peer;
|
||||
nip44CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip44OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44DecPeerEl.value);
|
||||
const ciphertext = String(nip44CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip44_decrypt", params, "web-nip44-dec");
|
||||
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
|
||||
} catch (e) {
|
||||
nip44DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
examples/get_pubkey_fips.py
Normal file
64
examples/get_pubkey_fips.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
from coincurve import PrivateKey
|
||||
|
||||
HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips"
|
||||
PORT = 11111
|
||||
|
||||
# Demo caller key (32 bytes). Replace with your stable caller key in real use.
|
||||
PRIVKEY = bytes(range(1, 33))
|
||||
|
||||
params = []
|
||||
body_hash = hashlib.sha256(json.dumps(params, separators=(",", ":")).encode()).hexdigest()
|
||||
|
||||
sk = PrivateKey(PRIVKEY)
|
||||
pubkey_x = sk.public_key.format(compressed=False)[1:33].hex()
|
||||
created_at = int(time.time())
|
||||
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", "get_public_key"],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
|
||||
content = "py-min"
|
||||
serialized_event = json.dumps(
|
||||
[0, pubkey_x, created_at, 27235, tags, content], separators=(",", ":"), ensure_ascii=False
|
||||
).encode()
|
||||
event_id = hashlib.sha256(serialized_event).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
request = {
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": params,
|
||||
"auth": {
|
||||
"id": event_id,
|
||||
"pubkey": pubkey_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": content,
|
||||
"sig": sig,
|
||||
},
|
||||
}
|
||||
|
||||
payload = json.dumps(request, separators=(",", ":")).encode()
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
|
||||
with socket.create_connection((HOST, PORT), timeout=10) as s:
|
||||
s.sendall(frame)
|
||||
hdr = b""
|
||||
while len(hdr) < 4:
|
||||
hdr += s.recv(4 - len(hdr))
|
||||
ln = struct.unpack(">I", hdr)[0]
|
||||
body = b""
|
||||
while len(body) < ln:
|
||||
body += s.recv(ln - len(body))
|
||||
|
||||
print(json.loads(body.decode())["result"])
|
||||
110
examples/get_pubkey_qrexec.c
Normal file
110
examples/get_pubkey_qrexec.c
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* get_pubkey_qrexec.c — connect to a running n_signer in another Qubes qube
|
||||
* via qrexec, using the high-level nostr_signer API from nostr_core_lib.
|
||||
*
|
||||
* This demonstrates the new nostr_core_lib client features:
|
||||
* - nostr_signer_nsigner_qrexec() — qrexec transport
|
||||
* - nostr_signer_nsigner_set_nostr_index() — index-based key selection
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_qrexec <target_qube> [nostr_index]
|
||||
* ./get_pubkey_qrexec nostr_signer 0
|
||||
* ./get_pubkey_qrexec nostr_signer 1
|
||||
*
|
||||
* No auth envelope needed — qrexec identity comes from QREXEC_REMOTE_DOMAIN
|
||||
* on the server side.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "nip019.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *target_qube;
|
||||
const char *service_name = "qubes.NsignerRpc";
|
||||
int nostr_index = 0;
|
||||
nostr_signer_t *signer = NULL;
|
||||
char pubkey_hex[65];
|
||||
unsigned char pubkey_bytes[32];
|
||||
char npub[128];
|
||||
int rc;
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
target_qube = argv[1];
|
||||
if (argc > 2) {
|
||||
nostr_index = atoi(argv[2]);
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to n_signer in qube \"%s\" via qrexec (index %d)...\n",
|
||||
target_qube, nostr_index);
|
||||
|
||||
/* Create a high-level signer backed by qrexec transport */
|
||||
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
||||
if (signer == NULL) {
|
||||
fprintf(stderr, "failed to create qrexec signer (is qrexec-client-vm available?)\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
||||
if (nostr_signer_nsigner_set_nostr_index(signer, nostr_index) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to set nostr_index\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Request the public key */
|
||||
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
if (rc == NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED) {
|
||||
fprintf(stderr, "DENIED: index %d is not in the signer's whitelist\n", nostr_index);
|
||||
} else if (rc == NOSTR_ERROR_NSIGNER_POLICY_DENIED) {
|
||||
fprintf(stderr, "DENIED: policy denied (caller not approved at signer terminal)\n");
|
||||
} else {
|
||||
fprintf(stderr, "get_public_key failed: error code %d\n", rc);
|
||||
}
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Convert hex pubkey to npub (bech32) */
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 32; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(pubkey_hex + 2 * i, "%2x", &byte) != 1) {
|
||||
fprintf(stderr, "failed to parse hex pubkey\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
pubkey_bytes[i] = (unsigned char)byte;
|
||||
}
|
||||
}
|
||||
if (nostr_key_to_bech32(pubkey_bytes, "npub", npub) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to convert to npub\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("index %d: hex=%s npub=%s\n", nostr_index, pubkey_hex, npub);
|
||||
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
195
examples/get_pubkey_tcp.c
Normal file
195
examples/get_pubkey_tcp.c
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* get_pubkey_tcp.c — connect to a running n_signer over TCP and call
|
||||
* get_public_key for nostr_index 0 and 1, printing both hex pubkey and
|
||||
* bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_tcp <host> <port>
|
||||
* ./get_pubkey_tcp npub1xxx...fips 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Output: for each index, prints:
|
||||
* index 0: hex=<64 hex chars> npub=npub1...
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "nip019.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* Demo caller key (32 bytes). Replace with your stable caller key in real use. */
|
||||
static const unsigned char DEMO_PRIVKEY[32] = {
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
|
||||
};
|
||||
|
||||
static int hex_to_bytes(const char *hex, unsigned char *out, size_t out_len) {
|
||||
size_t len = strlen(hex);
|
||||
if (len != out_len * 2) {
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < out_len; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
|
||||
return -1;
|
||||
}
|
||||
out[i] = (unsigned char)byte;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_pubkey(const char *host, int port, int nostr_index,
|
||||
char *out_hex, size_t hex_size, char *out_npub, size_t npub_size) {
|
||||
(void)npub_size; /* npub buffer size is enforced by nostr_key_to_bech32 output */
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
const char *hex_pubkey = NULL;
|
||||
unsigned char pubkey_bytes[32];
|
||||
int rc = -1;
|
||||
|
||||
transport = nsigner_transport_open_tcp(host, port, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open TCP transport to %s:%d\n", host, port);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* Set auth envelope — required for TCP listeners */
|
||||
if (nsigner_client_set_auth(client, DEMO_PRIVKEY, "get_pubkey_tcp") != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to set auth envelope\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* params: [{"nostr_index": N}] */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddNumberToObject(opts, "nostr_index", nostr_index);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed for index %d: %s\n", nostr_index,
|
||||
nsigner_client_last_error(client));
|
||||
params = NULL; /* nsigner_client_call took ownership even on failure */
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call took ownership */
|
||||
|
||||
if (!cJSON_IsString(result)) {
|
||||
fprintf(stderr, "index %d: unexpected result type\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hex_pubkey = result->valuestring;
|
||||
if (strlen(hex_pubkey) != 64) {
|
||||
fprintf(stderr, "index %d: unexpected pubkey length: %zu\n", nostr_index, strlen(hex_pubkey));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
strncpy(out_hex, hex_pubkey, hex_size - 1);
|
||||
out_hex[hex_size - 1] = '\0';
|
||||
|
||||
/* Convert hex pubkey to npub (bech32) */
|
||||
if (hex_to_bytes(hex_pubkey, pubkey_bytes, 32) != 0) {
|
||||
fprintf(stderr, "index %d: failed to parse hex pubkey\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
if (nostr_key_to_bech32(pubkey_bytes, "npub", out_npub) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "index %d: failed to convert to npub\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
cJSON_Delete(opts);
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(result);
|
||||
nsigner_client_free(client);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *host = "127.0.0.1";
|
||||
int port = 11111;
|
||||
char hex0[65], npub0[128];
|
||||
char hex1[65], npub1[128];
|
||||
int failures = 0;
|
||||
|
||||
if (argc > 1) {
|
||||
host = argv[1];
|
||||
}
|
||||
if (argc > 2) {
|
||||
port = atoi(argv[2]);
|
||||
if (port <= 0 || port > 65535) {
|
||||
fprintf(stderr, "Invalid port: %s\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to n_signer at %s:%d\n", host, port);
|
||||
printf("Querying get_public_key for nostr_index 0 and 1...\n\n");
|
||||
|
||||
/* Query index 0 */
|
||||
hex0[0] = '\0';
|
||||
npub0[0] = '\0';
|
||||
if (query_pubkey(host, port, 0, hex0, sizeof(hex0), npub0, sizeof(npub0)) == 0) {
|
||||
printf("index 0: hex=%s npub=%s\n", hex0, npub0);
|
||||
} else {
|
||||
printf("index 0: FAILED\n");
|
||||
failures++;
|
||||
}
|
||||
|
||||
/* Query index 1 */
|
||||
hex1[0] = '\0';
|
||||
npub1[0] = '\0';
|
||||
if (query_pubkey(host, port, 1, hex1, sizeof(hex1), npub1, sizeof(npub1)) == 0) {
|
||||
printf("index 1: hex=%s npub=%s\n", hex1, npub1);
|
||||
} else {
|
||||
printf("index 1: FAILED\n");
|
||||
failures++;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
|
||||
if (failures > 0) {
|
||||
printf("\n%d query(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\nAll queries succeeded\n");
|
||||
return 0;
|
||||
}
|
||||
100
examples/get_public_key_client.c
Normal file
100
examples/get_public_key_client.c
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* get_public_key_client.c — connect to a running n_signer over its abstract
|
||||
* UNIX socket and call get_public_key, using the shared nsigner client from
|
||||
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
|
||||
*
|
||||
* Usage: ./get_public_key_client [socket_name]
|
||||
*
|
||||
* Output: the raw JSON-RPC response string, e.g.
|
||||
* {"id":"1","result":"<pubkey hex>"}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *response = NULL;
|
||||
char *response_json = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 5000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
/* nsigner_client_new takes ownership of transport on success only */
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* params: [] (empty array — server picks the default role) */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call takes ownership of params */
|
||||
|
||||
/*
|
||||
* Reconstruct a JSON-RPC response string so the CLI output stays
|
||||
* backward-compatible with the old client example:
|
||||
* {"id":"<id>","result":"<pubkey hex>"}
|
||||
* nsigner_client_call returns only the parsed `result` element and does
|
||||
* not expose the server-assigned id, so we emit a minimal envelope.
|
||||
*/
|
||||
response = cJSON_CreateObject();
|
||||
if (response == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddStringToObject(response, "id", "1");
|
||||
cJSON_AddItemReferenceToObject(response, "result", result);
|
||||
|
||||
response_json = cJSON_PrintUnformatted(response);
|
||||
if (response_json == NULL) {
|
||||
fprintf(stderr, "failed to serialize response\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response_json);
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(response_json);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
nsigner_client_free(client); /* also closes/frees the transport */
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
214
examples/kb2040_hidden_signer_client.py
Normal file
214
examples/kb2040_hidden_signer_client.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
KB2040 Hidden Signer host client (WebUSB/vendor interface via pyusb).
|
||||
|
||||
Protocol:
|
||||
4-byte big-endian length + UTF-8 JSON body.
|
||||
|
||||
Default target:
|
||||
VID:PID 239a:8104
|
||||
|
||||
Examples:
|
||||
python3 examples/kb2040_hidden_signer_client.py status
|
||||
python3 examples/kb2040_hidden_signer_client.py set-mnemonic --mnemonic "abandon ... about"
|
||||
python3 examples/kb2040_hidden_signer_client.py get-public-key
|
||||
python3 examples/kb2040_hidden_signer_client.py sign-event --event '{"kind":1,"content":"hello"}'
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import usb.core
|
||||
import usb.util
|
||||
except ImportError:
|
||||
print("Missing pyusb. Install with: pip install pyusb", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def find_vendor_interface(dev):
|
||||
cfg = dev.get_active_configuration()
|
||||
for itf in cfg:
|
||||
if itf.bInterfaceClass == 0xFF:
|
||||
ep_out = None
|
||||
ep_in = None
|
||||
for ep in itf:
|
||||
direction = usb.util.endpoint_direction(ep.bEndpointAddress)
|
||||
if direction == usb.util.ENDPOINT_OUT:
|
||||
ep_out = ep
|
||||
elif direction == usb.util.ENDPOINT_IN:
|
||||
ep_in = ep
|
||||
if ep_out is not None and ep_in is not None:
|
||||
return itf, ep_out, ep_in
|
||||
return None, None, None
|
||||
|
||||
|
||||
def open_device(vid: int, pid: int):
|
||||
dev = usb.core.find(idVendor=vid, idProduct=pid)
|
||||
if dev is None:
|
||||
raise RuntimeError(f"Device {vid:04x}:{pid:04x} not found")
|
||||
|
||||
try:
|
||||
dev.set_configuration()
|
||||
except usb.core.USBError:
|
||||
pass
|
||||
|
||||
itf, ep_out, ep_in = find_vendor_interface(dev)
|
||||
if itf is None:
|
||||
raise RuntimeError("Vendor/WebUSB interface (class 0xFF) not found")
|
||||
|
||||
itf_num = itf.bInterfaceNumber
|
||||
try:
|
||||
if dev.is_kernel_driver_active(itf_num):
|
||||
dev.detach_kernel_driver(itf_num)
|
||||
except (NotImplementedError, usb.core.USBError):
|
||||
pass
|
||||
|
||||
usb.util.claim_interface(dev, itf_num)
|
||||
|
||||
# WebUSB-style connect handshake (CDC SET_CONTROL_LINE_STATE / request 0x22).
|
||||
# Firmware gates IN transfers on this "connected" state.
|
||||
try:
|
||||
dev.ctrl_transfer(0x21, 0x22, 0x0001, itf_num, None)
|
||||
except usb.core.USBError:
|
||||
# Some stacks may not require/implement this; continue and let RPC I/O decide.
|
||||
pass
|
||||
|
||||
return dev, itf_num, ep_out, ep_in
|
||||
|
||||
|
||||
def close_device(dev, itf_num: int):
|
||||
try:
|
||||
usb.util.release_interface(dev, itf_num)
|
||||
except usb.core.USBError:
|
||||
pass
|
||||
|
||||
|
||||
_rx_remainder = bytearray()
|
||||
|
||||
|
||||
def send_frame(ep_out, payload: bytes):
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
ep_out.write(frame)
|
||||
|
||||
|
||||
def recv_exact(ep_in, n: int, timeout_ms: int):
|
||||
global _rx_remainder
|
||||
|
||||
out = bytearray()
|
||||
|
||||
# Consume previously over-read bytes first.
|
||||
if _rx_remainder:
|
||||
take = min(n, len(_rx_remainder))
|
||||
out.extend(_rx_remainder[:take])
|
||||
del _rx_remainder[:take]
|
||||
|
||||
deadline = time.time() + max(1.0, timeout_ms / 1000.0)
|
||||
packet_size = max(64, int(getattr(ep_in, "wMaxPacketSize", 64)))
|
||||
|
||||
while len(out) < n and time.time() < deadline:
|
||||
chunk = ep_in.read(packet_size, timeout=timeout_ms)
|
||||
out.extend(bytes(chunk))
|
||||
|
||||
if len(out) < n:
|
||||
raise TimeoutError(f"Timeout while reading {n} bytes (got {len(out)})")
|
||||
|
||||
if len(out) > n:
|
||||
_rx_remainder.extend(out[n:])
|
||||
del out[n:]
|
||||
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def recv_frame(ep_in, timeout_ms: int):
|
||||
hdr = recv_exact(ep_in, 4, timeout_ms)
|
||||
body_len = struct.unpack(">I", hdr)[0]
|
||||
if body_len > 64 * 1024:
|
||||
raise ValueError(f"Refusing oversized frame: {body_len}")
|
||||
body = recv_exact(ep_in, body_len, timeout_ms)
|
||||
return body
|
||||
|
||||
|
||||
def rpc(dev_args, method: str, params: dict):
|
||||
global _rx_remainder
|
||||
|
||||
dev, itf_num, ep_out, ep_in = open_device(dev_args.vid, dev_args.pid)
|
||||
try:
|
||||
_rx_remainder.clear()
|
||||
req = {"method": method}
|
||||
req.update(params)
|
||||
payload = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
send_frame(ep_out, payload)
|
||||
resp = recv_frame(ep_in, dev_args.timeout_ms)
|
||||
return json.loads(resp.decode("utf-8", errors="replace"))
|
||||
finally:
|
||||
close_device(dev, itf_num)
|
||||
|
||||
|
||||
def cmd_status(args):
|
||||
print(json.dumps(rpc(args, "get_status", {}), indent=2))
|
||||
|
||||
|
||||
def cmd_set_mnemonic(args):
|
||||
print(json.dumps(rpc(args, "set_mnemonic", {"mnemonic": args.mnemonic}), indent=2))
|
||||
|
||||
|
||||
def cmd_set_auto_approve(args):
|
||||
value = args.value.lower() in ("1", "true", "yes", "on")
|
||||
print(json.dumps(rpc(args, "set_auto_approve", {"value": value}), indent=2))
|
||||
|
||||
|
||||
def cmd_get_public_key(args):
|
||||
print(json.dumps(rpc(args, "get_public_key", {}), indent=2))
|
||||
|
||||
|
||||
def cmd_sign_event(args):
|
||||
event = json.loads(args.event)
|
||||
print(json.dumps(rpc(args, "nostr_sign_event", {"event": event}), indent=2))
|
||||
|
||||
|
||||
def build_parser():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--vid", type=lambda s: int(s, 16), default=0x239A, help="USB VID in hex (default: 239A)")
|
||||
p.add_argument("--pid", type=lambda s: int(s, 16), default=0x8104, help="USB PID in hex (default: 8104)")
|
||||
p.add_argument("--timeout-ms", type=int, default=3000, help="USB read timeout in ms")
|
||||
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
s = sub.add_parser("status")
|
||||
s.set_defaults(func=cmd_status)
|
||||
|
||||
s = sub.add_parser("set-mnemonic")
|
||||
s.add_argument("--mnemonic", required=True)
|
||||
s.set_defaults(func=cmd_set_mnemonic)
|
||||
|
||||
s = sub.add_parser("set-auto-approve")
|
||||
s.add_argument("--value", required=True, help="true|false")
|
||||
s.set_defaults(func=cmd_set_auto_approve)
|
||||
|
||||
s = sub.add_parser("get-public-key")
|
||||
s.set_defaults(func=cmd_get_public_key)
|
||||
|
||||
s = sub.add_parser("sign-event")
|
||||
s.add_argument("--event", required=True, help="JSON object string")
|
||||
s.set_defaults(func=cmd_sign_event)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
args.func(args)
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
257
examples/n_signer_qube_example_fips.js
Normal file
257
examples/n_signer_qube_example_fips.js
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* n_signer_qube_example.js — connect to a running n_signer over TCP (FIPS mesh)
|
||||
* and call get_public_key for nostr_index 0 and 1, printing both hex pubkey
|
||||
* and bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* node n_signer_qube_example.js [host] [port]
|
||||
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Protocol:
|
||||
* - 4-byte big-endian length prefix + JSON payload (TCP framing)
|
||||
* - JSON-RPC: {"id":"...","method":"get_public_key","params":[{"nostr_index":N}],"auth":{...}}
|
||||
* - Auth envelope: kind 27235 Nostr event with nsigner_rpc, nsigner_method,
|
||||
* nsigner_body_hash tags. body_hash = sha256(json.dumps(params))
|
||||
* - Response: {"id":"...","result":"<pubkey hex>"} or {"id":"...","error":{...}}
|
||||
*/
|
||||
|
||||
const net = require("net");
|
||||
const crypto = require("crypto");
|
||||
const secp = require("@noble/secp256k1");
|
||||
|
||||
// @noble/secp256k1 v3 requires us to provide sync sha256/hmacSha256
|
||||
secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest());
|
||||
secp.hashes.hmacSha256 = (key, msg) =>
|
||||
new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest());
|
||||
|
||||
const { schnorr } = secp;
|
||||
|
||||
/** Convert a Uint8Array to a hex string. */
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
/** Convert a hex string to a Uint8Array. */
|
||||
function hexToBytes(hex) {
|
||||
const arr = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Demo caller key (32 bytes). Replace with your stable caller key in real use.
|
||||
const DEMO_PRIVKEY = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1));
|
||||
|
||||
/**
|
||||
* Compute the body hash for a params array.
|
||||
* This must match n_signer's hashing: sha256 of canonical JSON (no spaces).
|
||||
*/
|
||||
function computeBodyHash(params) {
|
||||
const canonical = JSON.stringify(params);
|
||||
return crypto.createHash("sha256").update(canonical).digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a kind-27235 auth envelope for the given method and params.
|
||||
* Returns the auth object to include in the JSON-RPC request.
|
||||
*/
|
||||
function buildAuthEnvelope(privkey, method, params, requestId) {
|
||||
const bodyHash = computeBodyHash(params);
|
||||
const pubkeyHex = bytesToHex(schnorr.getPublicKey(privkey));
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
// nsigner_rpc tag value MUST match the JSON-RPC request "id" field
|
||||
const tags = [
|
||||
["nsigner_rpc", requestId],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
|
||||
const content = "js-example";
|
||||
|
||||
// Serialize the event for signing: [0, pubkey, created_at, kind, tags, content]
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
pubkeyHex,
|
||||
createdAt,
|
||||
27235,
|
||||
tags,
|
||||
content,
|
||||
]);
|
||||
|
||||
const eventId = crypto.createHash("sha256").update(serialized).digest("hex");
|
||||
const sig = bytesToHex(schnorr.sign(hexToBytes(eventId), privkey, new Uint8Array(32)));
|
||||
|
||||
return {
|
||||
id: eventId,
|
||||
pubkey: pubkeyHex,
|
||||
created_at: createdAt,
|
||||
kind: 27235,
|
||||
tags: tags,
|
||||
content: content,
|
||||
sig: sig,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a framed JSON-RPC request over a TCP socket and receive the response.
|
||||
* Framing: 4-byte big-endian length prefix + JSON payload.
|
||||
*/
|
||||
function sendRequest(socket, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = Buffer.from(JSON.stringify(request), "utf8");
|
||||
const header = Buffer.alloc(4);
|
||||
header.writeUInt32BE(payload.length, 0);
|
||||
socket.write(Buffer.concat([header, payload]));
|
||||
|
||||
let headerBuf = Buffer.alloc(0);
|
||||
let bodyBuf = Buffer.alloc(0);
|
||||
let bodyLen = 0;
|
||||
let state = "header";
|
||||
|
||||
const onData = (chunk) => {
|
||||
if (state === "header") {
|
||||
headerBuf = Buffer.concat([headerBuf, chunk]);
|
||||
if (headerBuf.length >= 4) {
|
||||
bodyLen = headerBuf.readUInt32BE(0);
|
||||
const remaining = headerBuf.subarray(4);
|
||||
headerBuf = Buffer.alloc(0);
|
||||
state = "body";
|
||||
if (remaining.length > 0) {
|
||||
bodyBuf = Buffer.concat([bodyBuf, remaining]);
|
||||
}
|
||||
if (bodyBuf.length >= bodyLen) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
} else if (state === "body") {
|
||||
bodyBuf = Buffer.concat([bodyBuf, chunk]);
|
||||
if (bodyBuf.length >= bodyLen) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function finish() {
|
||||
socket.off("data", onData);
|
||||
socket.off("error", onError);
|
||||
const body = bodyBuf.subarray(0, bodyLen).toString("utf8");
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse response: ${e.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
function onError(err) {
|
||||
socket.off("data", onData);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
socket.on("data", onData);
|
||||
socket.on("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query get_public_key for a given nostr_index.
|
||||
* Opens a fresh TCP connection for each request (n_signer handles one request
|
||||
* per connection).
|
||||
*/
|
||||
async function getPublicKey(host, port, nostrIndex, privkey) {
|
||||
const params = [{ nostr_index: nostrIndex }];
|
||||
const requestId = String(nostrIndex);
|
||||
const auth = buildAuthEnvelope(privkey, "get_public_key", params, requestId);
|
||||
|
||||
const request = {
|
||||
id: requestId,
|
||||
method: "get_public_key",
|
||||
params: params,
|
||||
auth: auth,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new net.Socket();
|
||||
socket.setTimeout(15000);
|
||||
|
||||
socket.connect(port, host, async () => {
|
||||
try {
|
||||
const response = await sendRequest(socket, request);
|
||||
socket.destroy();
|
||||
resolve(response);
|
||||
} catch (e) {
|
||||
socket.destroy();
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
reject(new Error("Connection timed out"));
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a 32-byte hex pubkey to bech32 npub format (NIP-19).
|
||||
*/
|
||||
function hexToNpub(pubkeyHex) {
|
||||
const { nip19 } = require("nostr-tools");
|
||||
return nip19.npubEncode(pubkeyHex);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const host = process.argv[2] || "127.0.0.1";
|
||||
const port = parseInt(process.argv[3] || "11111", 10);
|
||||
|
||||
console.log(`Connecting to n_signer at ${host}:${port}`);
|
||||
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
|
||||
|
||||
let failures = 0;
|
||||
|
||||
for (const index of [0, 1]) {
|
||||
// Small delay between requests to avoid auth nonce collision
|
||||
// (the auth envelope uses created_at as part of the nonce)
|
||||
if (index > 0) await new Promise((r) => setTimeout(r, 1100));
|
||||
try {
|
||||
const response = await getPublicKey(host, port, index, DEMO_PRIVKEY);
|
||||
|
||||
if (response.error) {
|
||||
console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pubkeyHex = response.result;
|
||||
const npub = hexToNpub(pubkeyHex);
|
||||
console.log(`index ${index}: hex=${pubkeyHex} npub=${npub}`);
|
||||
} catch (e) {
|
||||
console.log(`index ${index}: FAILED - ${e.message}`);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.log(`\n${failures} query(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("\nAll queries succeeded");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
155
examples/n_signer_qube_example_qrexec.js
Normal file
155
examples/n_signer_qube_example_qrexec.js
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* n_signer_qube_example_qrexec.js — connect to n_signer in the nostr_signer
|
||||
* qube via Qubes qrexec (qubes.NsignerRpc service) and call get_public_key
|
||||
* for nostr_index 0 and 1, printing both hex pubkey and bech32 npub.
|
||||
*
|
||||
* This uses Qubes OS's built-in secure IPC (qrexec) instead of FIPS TCP.
|
||||
* No network connectivity is required — all traffic stays within the host.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - The qubes.NsignerRpc service must be installed in the nostr_signer qube
|
||||
* (see packaging/qubes/install-service.sh)
|
||||
* - The qrexec policy must be installed in dom0
|
||||
* (see packaging/qubes/install-policy.sh)
|
||||
* - The nostr_signer qube must be tagged with 'nsigner-signer'
|
||||
* (qvm-tags nostr_signer add nsigner-signer)
|
||||
* - A mnemonic file must exist at /home/user/.nsigner_mnemonic in the
|
||||
* nostr_signer qube
|
||||
*
|
||||
* Usage:
|
||||
* node n_signer_qube_example_qrexec.js [target_qube]
|
||||
* node n_signer_qube_example_qrexec.js nostr_signer
|
||||
*
|
||||
* If no argument is given, defaults to "nostr_signer".
|
||||
*
|
||||
* Protocol:
|
||||
* - qrexec-client-vm spawns the qubes.NsignerRpc service in the target qube
|
||||
* - The service runs: nsigner --listen qrexec --allow-all --mnemonic-fd 3
|
||||
* - We send one framed JSON-RPC request via stdin, receive one framed response via stdout
|
||||
* - Framing: 4-byte big-endian length prefix + JSON payload
|
||||
* - Auth: not required in qrexec mode (caller identity is qubes:<source-vm>)
|
||||
*/
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
const { nip19 } = require("nostr-tools");
|
||||
|
||||
/**
|
||||
* Frame a JSON-RPC request: 4-byte big-endian length + JSON payload.
|
||||
*/
|
||||
function frameRequest(obj) {
|
||||
const payload = Buffer.from(JSON.stringify(obj), "utf8");
|
||||
const header = Buffer.alloc(4);
|
||||
header.writeUInt32BE(payload.length, 0);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a framed response from the qrexec stdout buffer.
|
||||
*/
|
||||
function parseFramedResponse(buf) {
|
||||
if (buf.length < 4) {
|
||||
throw new Error("short response (missing frame header)");
|
||||
}
|
||||
const len = buf.readUInt32BE(0);
|
||||
const payload = buf.subarray(4, 4 + len);
|
||||
if (payload.length !== len) {
|
||||
throw new Error(`short response payload: expected ${len}, got ${payload.length}`);
|
||||
}
|
||||
return JSON.parse(payload.toString("utf8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call nsigner via qrexec. Sends one framed request, receives one framed response.
|
||||
* Each call spawns a fresh qrexec-client-vm process (one request per invocation).
|
||||
*/
|
||||
function callNsignerQrexec(targetQube, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const framed = frameRequest(request);
|
||||
const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
|
||||
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
||||
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`));
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
||||
reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = parseFramedResponse(Buffer.concat(stdoutChunks));
|
||||
resolve(response);
|
||||
} catch (e) {
|
||||
reject(new Error(`failed to parse response: ${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
// Send the framed request and close stdin
|
||||
proc.stdin.write(framed);
|
||||
proc.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query get_public_key for a given nostr_index via qrexec.
|
||||
* No auth envelope needed — qrexec mode uses qubes:<source-vm> as caller identity.
|
||||
*/
|
||||
async function getPublicKey(targetQube, nostrIndex) {
|
||||
const request = {
|
||||
id: String(nostrIndex),
|
||||
method: "get_public_key",
|
||||
params: [{ nostr_index: nostrIndex }],
|
||||
};
|
||||
|
||||
return callNsignerQrexec(targetQube, request);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targetQube = process.argv[2] || "nostr_signer";
|
||||
|
||||
console.log(`Calling n_signer in qube "${targetQube}" via qrexec...`);
|
||||
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
|
||||
|
||||
let failures = 0;
|
||||
|
||||
for (const index of [0, 1]) {
|
||||
try {
|
||||
const response = await getPublicKey(targetQube, index);
|
||||
|
||||
if (response.error) {
|
||||
console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pubkeyHex = response.result;
|
||||
const npub = nip19.npubEncode(pubkeyHex);
|
||||
console.log(`index ${index}: hex=${pubkeyHex} npub=${npub}`);
|
||||
} catch (e) {
|
||||
console.log(`index ${index}: FAILED - ${e.message}`);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.log(`\n${failures} query(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("\nAll queries succeeded");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
219
examples/otp_nostr_30078.py
Normal file
219
examples/otp_nostr_30078.py
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
otp_nostr_30078.py — example: encrypt data with OTP, wrap in a Nostr kind 30078
|
||||
event, sign it with n_signer, and print the signed event for publishing.
|
||||
|
||||
Workflow:
|
||||
1. Call n_signer's `otp_encrypt` verb to encrypt plaintext with the bound OTP pad.
|
||||
2. Build a Nostr kind 30078 (replaceable parameterized) event with the ASCII-armored
|
||||
ciphertext as the `content` field.
|
||||
3. Call n_signer's `sign_event` verb to sign the event with the secp256k1 key.
|
||||
4. Print the signed event JSON, ready to publish to Nostr relays.
|
||||
|
||||
This is a demo — it does not actually publish to a relay. To publish, send the
|
||||
signed event to your preferred Nostr relay using a library like nostr-tools,
|
||||
nostril, or nak.
|
||||
|
||||
Usage:
|
||||
python3 examples/otp_nostr_30078.py "Your secret message here"
|
||||
|
||||
Requirements:
|
||||
- n_signer running with --otp-pad-dir / --otp-pad bound, and a secp256k1
|
||||
role (e.g. "main") available for sign_event.
|
||||
- This script connects to n_signer via stdio (one process per request).
|
||||
|
||||
See plans/otp_nostr_integration.md for the full design.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
NSIGNER = "./build/nsigner"
|
||||
PAD_DIR = "/media/user/Music/pads"
|
||||
PAD_SPEC = "333e9902db839d9d"
|
||||
MNEMONIC_FILE = ".test_mnemonic"
|
||||
MNEMONIC_TMP = ".test_mnemonic_otp_30078.tmp"
|
||||
|
||||
|
||||
def send_framed(proc, obj):
|
||||
payload = json.dumps(obj).encode()
|
||||
proc.stdin.write(struct.pack(">I", len(payload)))
|
||||
proc.stdin.write(payload)
|
||||
proc.stdin.flush()
|
||||
|
||||
|
||||
def recv_framed(proc):
|
||||
"""Read a framed response, skipping any banner text on stdout."""
|
||||
buf = b""
|
||||
while True:
|
||||
b = proc.stdout.read(1)
|
||||
if not b:
|
||||
return None
|
||||
buf = (buf + b)[-4:]
|
||||
if len(buf) < 4:
|
||||
continue
|
||||
(length,) = struct.unpack(">I", buf)
|
||||
if 1 <= length <= 1024 * 1024:
|
||||
peek = proc.stdout.read(1)
|
||||
if peek == b"{":
|
||||
body = peek + proc.stdout.read(length - 1)
|
||||
return json.loads(body.decode())
|
||||
else:
|
||||
buf = (buf + peek)[-4:]
|
||||
|
||||
|
||||
def run_one_request(req_obj):
|
||||
"""Run nsigner in stdio mode for a single framed request/response."""
|
||||
shell_cmd = (
|
||||
f"exec 3<{MNEMONIC_TMP}; "
|
||||
f"exec {NSIGNER} --listen stdio --mnemonic-fd 3 "
|
||||
f"--otp-pad-dir {PAD_DIR} --otp-pad {PAD_SPEC} "
|
||||
f"--otp-allow-blkback --allow-all"
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
["bash", "-c", shell_cmd],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
import time as _time
|
||||
_time.sleep(1.0)
|
||||
if proc.poll() is not None:
|
||||
err = proc.stderr.read().decode()
|
||||
print(f"ERROR: nsigner exited early (code {proc.returncode})")
|
||||
print(f"stderr: {err}")
|
||||
return None
|
||||
send_framed(proc, req_obj)
|
||||
resp = recv_framed(proc)
|
||||
proc.stdin.close()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return resp
|
||||
|
||||
|
||||
def compute_event_id(event):
|
||||
"""Compute the Nostr event ID (SHA-256 of the canonical serialized event)."""
|
||||
# Nostr event serialization: [0, pubkey, created_at, kind, tags, content]
|
||||
serialized = json.dumps([
|
||||
0,
|
||||
event["pubkey"],
|
||||
event["created_at"],
|
||||
event["kind"],
|
||||
event["tags"],
|
||||
event["content"],
|
||||
], separators=(",", ":"), ensure_ascii=False)
|
||||
return hashlib.sha256(serialized.encode()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
plaintext = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Secret OTP message"
|
||||
print(f"Plaintext: {plaintext}")
|
||||
|
||||
# Prepare the mnemonic temp file.
|
||||
with open(MNEMONIC_FILE) as f:
|
||||
mnemonic = f.read().strip()
|
||||
with open(MNEMONIC_TMP, "w") as f:
|
||||
f.write(mnemonic + "\n")
|
||||
|
||||
try:
|
||||
# Step 1: Get the public key for the "main" role
|
||||
print("\n=== Step 1: get_public_key ===")
|
||||
resp = run_one_request({
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: get_public_key failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
# The result is a plain hex string for secp256k1 backward compat.
|
||||
pubkey_hex = resp["result"].strip('"')
|
||||
print(f"Public key: {pubkey_hex}")
|
||||
|
||||
# Step 2: Encrypt the plaintext with OTP
|
||||
print("\n=== Step 2: otp_encrypt ===")
|
||||
pt_b64 = base64.b64encode(plaintext.encode()).decode()
|
||||
resp = run_one_request({
|
||||
"id": "2",
|
||||
"method": "encrypt",
|
||||
"params": [pt_b64, {"algorithm": "otp", "encoding": "ascii"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: otp_encrypt failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
enc_result = json.loads(resp["result"])
|
||||
ciphertext = enc_result["ciphertext"]
|
||||
pad_chksum = enc_result["pad_chksum"]
|
||||
pad_offset = enc_result["pad_offset_after"]
|
||||
print(f"Pad checksum: {pad_chksum}")
|
||||
print(f"Pad offset after encrypt: {pad_offset}")
|
||||
print(f"Ciphertext (first 60 chars): {ciphertext[:60]}...")
|
||||
|
||||
# Step 3: Build the Nostr kind 30078 event
|
||||
print("\n=== Step 3: Build kind 30078 event ===")
|
||||
# Use a unique d-tag based on the pad checksum and offset.
|
||||
d_tag = f"otp-{pad_chksum[:16]}-{pad_offset}"
|
||||
event = {
|
||||
"pubkey": pubkey_hex,
|
||||
"created_at": int(time.time()),
|
||||
"kind": 30078,
|
||||
"tags": [
|
||||
["d", d_tag],
|
||||
["otp-pad", pad_chksum[:16]],
|
||||
["otp-version", "v0.0.2-otp"],
|
||||
["otp-encoding", "ascii"],
|
||||
],
|
||||
"content": ciphertext,
|
||||
}
|
||||
# Compute the event ID.
|
||||
event_id = compute_event_id(event)
|
||||
event["id"] = event_id
|
||||
print(f"Event ID: {event_id}")
|
||||
print(f"d-tag: {d_tag}")
|
||||
|
||||
# Step 4: Sign the event with n_signer
|
||||
print("\n=== Step 4: sign_event ===")
|
||||
# sign_event expects the event JSON as the first param (without id/sig).
|
||||
# The signer computes the id and signature internally.
|
||||
event_for_signing = {
|
||||
"pubkey": event["pubkey"],
|
||||
"created_at": event["created_at"],
|
||||
"kind": event["kind"],
|
||||
"tags": event["tags"],
|
||||
"content": event["content"],
|
||||
}
|
||||
resp = run_one_request({
|
||||
"id": "3",
|
||||
"method": "nostr_sign_event",
|
||||
"params": [json.dumps(event_for_signing), {"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: sign_event failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
sig = resp["result"].strip('"')
|
||||
event["sig"] = sig
|
||||
print(f"Signature: {sig[:60]}...")
|
||||
|
||||
# Step 5: Print the signed event
|
||||
print("\n=== Signed Nostr event (ready to publish) ===")
|
||||
print(json.dumps(event, indent=2))
|
||||
print(f"\nTo publish: send this event to a Nostr relay.")
|
||||
print(f"To decrypt: call otp_decrypt with the content field.")
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
os.unlink(MNEMONIC_TMP)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
276
examples/pq_kem_example.c
Normal file
276
examples/pq_kem_example.c
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* pq_kem_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate post-quantum key encapsulation with ML-KEM-768.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an ML-KEM-768 role ("kem_main").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends a kem_encapsulate request with the public key, obtaining a
|
||||
* ciphertext + shared secret.
|
||||
* 4. Sends a kem_decapsulate request with the ciphertext, recovering the
|
||||
* shared secret on the signer side.
|
||||
* 5. Prints both shared secrets — they should match.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=pq-kem,
|
||||
* curve=ml-kem-768, named "kem_main" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./pq_kem_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: kem_main
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
char **out_pub_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_pub_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(parsed, "public_key");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
*out_pub_hex = strdup(pk_item->valuestring);
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* kem_encapsulate: returns ciphertext_hex and shared_secret_hex (newly
|
||||
* allocated, caller frees). */
|
||||
static int kem_encapsulate(nsigner_client_t *client, const char *role,
|
||||
const char *pub_hex,
|
||||
char **out_ct_hex, char **out_ss_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_ct_hex = NULL;
|
||||
*out_ss_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(pub_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "encapsulate", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *ct_item = cJSON_GetObjectItemCaseSensitive(parsed, "ciphertext");
|
||||
cJSON *ss_item = cJSON_GetObjectItemCaseSensitive(parsed, "shared_secret");
|
||||
if (cJSON_IsString(ct_item) && cJSON_IsString(ss_item)) {
|
||||
*out_ct_hex = strdup(ct_item->valuestring);
|
||||
*out_ss_hex = strdup(ss_item->valuestring);
|
||||
if (*out_ct_hex != NULL && *out_ss_hex != NULL) {
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *kem_decapsulate(nsigner_client_t *client, const char *role,
|
||||
const char *ct_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *ss_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(ct_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "decapsulate", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *ss_item = cJSON_GetObjectItemCaseSensitive(parsed, "shared_secret");
|
||||
if (cJSON_IsString(ss_item)) {
|
||||
ss_hex = strdup(ss_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return ss_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "kem_main";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
char *pub_hex = NULL;
|
||||
char *ct_hex = NULL;
|
||||
char *encap_ss_hex = NULL;
|
||||
char *decap_ss_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== PQ KEM Example (ML-KEM-768) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the ML-KEM-768 public key. */
|
||||
if (get_structured_pubkey(client, role, &pub_hex) != 0 || pub_hex == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Public Key:\n");
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pub_hex), strlen(pub_hex) / 2);
|
||||
printf(" pub_head: %.64s...\n", pub_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 2. Encapsulate with the public key. */
|
||||
printf("Encapsulating with public key...\n");
|
||||
if (kem_encapsulate(client, role, pub_hex, &ct_hex, &encap_ss_hex) != 0) {
|
||||
fprintf(stderr, "kem_encapsulate failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Ciphertext:\n");
|
||||
printf(" ct_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(ct_hex), strlen(ct_hex) / 2);
|
||||
printf(" ct_head: %.64s...\n", ct_hex);
|
||||
printf("Encapsulated shared secret:\n");
|
||||
printf(" ss: %s\n", encap_ss_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 3. Decapsulate with the ciphertext (uses the role's private key). */
|
||||
printf("Decapsulating ciphertext on signer side...\n");
|
||||
decap_ss_hex = kem_decapsulate(client, role, ct_hex);
|
||||
if (decap_ss_hex == NULL) {
|
||||
fprintf(stderr, "kem_decapsulate failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Decapsulated shared secret:\n");
|
||||
printf(" ss: %s\n", decap_ss_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 4. Verify the shared secrets match. */
|
||||
if (strcmp(encap_ss_hex, decap_ss_hex) == 0) {
|
||||
printf("SUCCESS: shared secrets match!\n");
|
||||
rc = 0;
|
||||
} else {
|
||||
printf("FAILURE: shared secrets do NOT match!\n");
|
||||
}
|
||||
|
||||
cleanup:
|
||||
free(pub_hex);
|
||||
free(ct_hex);
|
||||
free(encap_ss_hex);
|
||||
free(decap_ss_hex);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
217
examples/pq_sign_example.c
Normal file
217
examples/pq_sign_example.c
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* pq_sign_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate post-quantum signing with ML-DSA-65.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an ML-DSA-65 role ("pq_sig").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends a sign_data request with a test message.
|
||||
* 4. Prints the signature (hex) and algorithm.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=pq-sig,
|
||||
* curve=ml-dsa-65, named "pq_sig" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./pq_sign_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: pq_sig
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
cJSON **out_obj) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_obj = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
/* result is a cJSON string containing the serialized structured object. */
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL && cJSON_IsObject(parsed)) {
|
||||
*out_obj = parsed;
|
||||
parsed = NULL;
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *sign_data(nsigner_client_t *client, const char *role,
|
||||
const char *msg_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *sig_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
/* result is a string containing {"signature":"<hex>","algorithm":"<alg>"} */
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *sig_item = cJSON_GetObjectItemCaseSensitive(parsed, "signature");
|
||||
if (cJSON_IsString(sig_item)) {
|
||||
sig_hex = strdup(sig_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return sig_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "pq_sig";
|
||||
/* "hello post-quantum world" in hex */
|
||||
const char *msg_hex = "68656c6c6f20706f73742d7175616e74756d20776f726c64";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *pubkey_obj = NULL;
|
||||
char *sig_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== PQ Sign Example (ML-DSA-65) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the structured public key. */
|
||||
if (get_structured_pubkey(client, role, &pubkey_obj) != 0 || pubkey_obj == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
cJSON *alg_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "algorithm");
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "public_key");
|
||||
cJSON *kid_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "key_id");
|
||||
|
||||
printf("Public Key:\n");
|
||||
printf(" algorithm: %s\n",
|
||||
(cJSON_IsString(alg_item)) ? alg_item->valuestring : "?");
|
||||
printf(" key_id: %s\n",
|
||||
(cJSON_IsString(kid_item)) ? kid_item->valuestring : "?");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
/* ML-DSA-65 public key is 3904 hex chars — print length + prefix. */
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pk_item->valuestring), strlen(pk_item->valuestring) / 2);
|
||||
printf(" pub_head: %.64s...\n", pk_item->valuestring);
|
||||
} else {
|
||||
printf(" public_key: (missing)\n");
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
/* 2. Sign a test message. */
|
||||
printf("Signing message (hex): %s\n", msg_hex);
|
||||
sig_hex = sign_data(client, role, msg_hex);
|
||||
if (sig_hex == NULL) {
|
||||
fprintf(stderr, "sign_data failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("Signature:\n");
|
||||
printf(" sig_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(sig_hex), strlen(sig_hex) / 2);
|
||||
printf(" sig_head: %.64s...\n", sig_hex);
|
||||
printf("\n");
|
||||
|
||||
printf("To verify externally, use ML-DSA-65 (FIPS 204) verify with the\n");
|
||||
printf("public key above, the message, and this signature.\n");
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(sig_hex);
|
||||
cJSON_Delete(pubkey_obj);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
114
examples/sign_event_client.c
Normal file
114
examples/sign_event_client.c
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* sign_event_client.c — connect to a running n_signer over its abstract
|
||||
* UNIX socket and call sign_event, using the shared nsigner client from
|
||||
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
|
||||
*
|
||||
* Usage: ./sign_event_client [socket_name]
|
||||
*
|
||||
* Output: the raw JSON-RPC response string, e.g.
|
||||
* {"id":"1","result":"{...signed event...}"}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}";
|
||||
const char *role = "main";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *response = NULL;
|
||||
char *response_json = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 5000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* params: [ "<event_json>", {"role":"main"} ] */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
if (role != NULL && role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL; /* owned by params now */
|
||||
|
||||
if (nsigner_client_call(client, "nostr_sign_event", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call takes ownership of params */
|
||||
|
||||
/*
|
||||
* Reconstruct a JSON-RPC response string for backward-compatible CLI
|
||||
* output: {"id":"1","result":"<signed event json>"}
|
||||
* The server returns the signed event as a JSON string (not an object),
|
||||
* so result is a cJSON string here.
|
||||
*/
|
||||
response = cJSON_CreateObject();
|
||||
if (response == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddStringToObject(response, "id", "1");
|
||||
cJSON_AddItemReferenceToObject(response, "result", result);
|
||||
|
||||
response_json = cJSON_PrintUnformatted(response);
|
||||
if (response_json == NULL) {
|
||||
fprintf(stderr, "failed to serialize response\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response_json);
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(response_json);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(opts);
|
||||
nsigner_client_free(client); /* also closes/frees the transport */
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
214
examples/ssh_sign_example.c
Normal file
214
examples/ssh_sign_example.c
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* ssh_sign_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate SSH signing with ed25519.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an SSH/ed25519 role ("ssh_main").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends an ssh_sign request with a test session ID.
|
||||
* 4. Prints the signature (hex) and algorithm.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=ssh,
|
||||
* curve=ed25519, named "ssh_main" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./ssh_sign_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: ssh_main
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
char **out_pub_hex, char **out_key_id) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_pub_hex = NULL;
|
||||
*out_key_id = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(parsed, "public_key");
|
||||
cJSON *kid_item = cJSON_GetObjectItemCaseSensitive(parsed, "key_id");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
*out_pub_hex = strdup(pk_item->valuestring);
|
||||
}
|
||||
if (cJSON_IsString(kid_item)) {
|
||||
*out_key_id = strdup(kid_item->valuestring);
|
||||
}
|
||||
if (*out_pub_hex != NULL) {
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *ssh_sign(nsigner_client_t *client, const char *role,
|
||||
const char *msg_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *sig_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *sig_item = cJSON_GetObjectItemCaseSensitive(parsed, "signature");
|
||||
if (cJSON_IsString(sig_item)) {
|
||||
sig_hex = strdup(sig_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return sig_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "ssh_main";
|
||||
/* A fake SSH session ID (32 bytes = 64 hex chars) for demonstration. */
|
||||
const char *session_id_hex =
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
char *pub_hex = NULL;
|
||||
char *key_id = NULL;
|
||||
char *sig_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== SSH Sign Example (ed25519) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the ed25519 public key. */
|
||||
if (get_structured_pubkey(client, role, &pub_hex, &key_id) != 0 ||
|
||||
pub_hex == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Public Key:\n");
|
||||
printf(" algorithm: ed25519\n");
|
||||
printf(" key_id: %s\n", (key_id != NULL) ? key_id : "?");
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pub_hex), strlen(pub_hex) / 2);
|
||||
printf(" pubkey: %s\n", pub_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 2. Sign a test SSH session ID. */
|
||||
printf("Signing SSH session ID (hex): %s\n", session_id_hex);
|
||||
sig_hex = ssh_sign(client, role, session_id_hex);
|
||||
if (sig_hex == NULL) {
|
||||
fprintf(stderr, "ssh_sign failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("Signature:\n");
|
||||
printf(" sig_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(sig_hex), strlen(sig_hex) / 2);
|
||||
printf(" sig: %s\n", sig_hex);
|
||||
printf("\n");
|
||||
|
||||
printf("This ed25519 signature can be verified with the public key above\n");
|
||||
printf("using standard ed25519 verify (e.g. libsodium, OpenSSL EVP_DigestVerify).\n");
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(pub_hex);
|
||||
free(key_id);
|
||||
free(sig_hex);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -1,22 +1,284 @@
|
||||
# Firmware Scaffolding
|
||||
# Feather S3 TFT Firmware
|
||||
|
||||
This directory is the placeholder for the future microcontroller variant of `nsigner`.
|
||||
This directory contains the ESP-IDF firmware for the Feather S3 TFT signer target.
|
||||
|
||||
## Current direction
|
||||
## USB transport mode
|
||||
|
||||
Initial MCU work should reuse platform abstractions that already exist in [`nostr_core_lib`](../nostr_core_lib/README.md), especially the ESP32 platform files under [`nostr_core_lib/platform/esp32/`](../nostr_core_lib/platform/esp32):
|
||||
Firmware now runs in **TinyUSB composite mode** (single USB peripheral owned by TinyUSB), exposing:
|
||||
|
||||
- [`nostr_platform_esp32.c`](../nostr_core_lib/platform/esp32/nostr_platform_esp32.c)
|
||||
- [`nostr_http_esp32.c`](../nostr_core_lib/platform/esp32/nostr_http_esp32.c)
|
||||
- [`nostr_websocket_esp32.c`](../nostr_core_lib/platform/esp32/nostr_websocket_esp32.c)
|
||||
- CDC-ACM interface for Python/serial tooling (`/dev/ttyACM*`)
|
||||
- Vendor interface for WebUSB browser transport
|
||||
|
||||
## Intended purpose
|
||||
Expected runtime USB identity:
|
||||
|
||||
This firmware target is for a USB signer and constrained-device signer profile, with `nsigner` protocol compatibility and transport adaptation (USB-CDC first, then other embedded transport options as needed).
|
||||
- VID:PID `303a:4001`
|
||||
- Product string: `Feather S3 TFT n_signer`
|
||||
|
||||
## Next milestones
|
||||
## Flashing workflow (important)
|
||||
|
||||
1. Add MCU-specific build system (ESP-IDF/CMake or equivalent).
|
||||
2. Add a minimal signer main loop with version reporting compatible with desktop `nsigner` semantics.
|
||||
3. Implement transport bridge framing compatible with host-side `nsigner-bridge` plans.
|
||||
4. Add hardware confirmation UX hooks (button + tiny display flow).
|
||||
Because TinyUSB owns USB at runtime, normal app mode is not the ROM USB-Serial/JTAG flashing interface.
|
||||
|
||||
Use this workflow for flashing:
|
||||
|
||||
1. Enter bootloader mode: hold **BOOT**, tap **RESET**, release **BOOT**.
|
||||
2. Confirm bootloader USB device appears (typically `/dev/ttyACM0`).
|
||||
3. Flash:
|
||||
|
||||
```bash
|
||||
cd firmware/feather_s3_tft
|
||||
idf.py -p /dev/ttyACM0 flash
|
||||
```
|
||||
|
||||
4. Exit bootloader and run app: ensure BOOT is released, then tap **RESET** once.
|
||||
5. Confirm app USB identity:
|
||||
|
||||
```bash
|
||||
lsusb | grep -i 303a
|
||||
# expect: 303a:4001 n_signer Feather S3 TFT n_signer
|
||||
```
|
||||
|
||||
## Quick validation
|
||||
|
||||
CDC path:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python ./examples/feather_get_public_key.py /dev/ttyACM0
|
||||
./.venv/bin/python ./examples/feather_sign_event.py /dev/ttyACM0
|
||||
```
|
||||
|
||||
WebUSB path:
|
||||
|
||||
- Open [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html)
|
||||
- Connect in Chrome/Edge
|
||||
- Run `get_public_key`
|
||||
- Confirm pubkey matches CDC result
|
||||
|
||||
## CYD (ESP32-2432S028) validation — Web Serial
|
||||
|
||||
The CYD has no native USB; its CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html):
|
||||
|
||||
- Open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) in Chrome/Edge
|
||||
- Click **Connect Web Serial**, select the CH340 port (`1a86:7523`)
|
||||
- Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp)
|
||||
- Each request shows the raw JSON-RPC request and response
|
||||
|
||||
The CYD firmware (v0.0.2+) speaks the same algorithm-based API as the host
|
||||
([`README.md`](../README.md) §4). The OTP pad is derived from the mnemonic
|
||||
seed (no USB pad on this board); the offset advances monotonically and is
|
||||
reported in every `encrypt`/`decrypt` response.
|
||||
|
||||
## Linux WebUSB host setup (one-time)
|
||||
|
||||
Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/udev/rules.d/99-nsigner-webusb.rules >/dev/null <<'EOF'
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", ATTR{idProduct}=="4001", MODE="0666", GROUP="plugdev", TAG+="uaccess"
|
||||
EOF
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
After installing the rule, unplug and replug the device once, then reload the demo page.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `lsusb` shows `303a:1001`, the board is still in ROM bootloader mode. Tap RESET with BOOT released.
|
||||
- If CDC command hangs, verify app-mode VID:PID is `303a:4001`, not `303a:1001`.
|
||||
- If no `/dev/ttyACM*` appears, unplug/replug after reset and re-check `lsusb`.
|
||||
- If WebUSB connect throws `SecurityError: Access denied`, install the Linux udev rule above and replug.
|
||||
|
||||
## CYD ESP32-2432S028 (CH340) serial-reset hardware note
|
||||
|
||||
For the CYD board (`firmware/cyd_esp32_2432s028`), opening `/dev/ttyUSB0` can reset the ESP32 because CH340 DTR/RTS control lines are wired into the ESP32 auto-reset circuit.
|
||||
|
||||
Symptoms:
|
||||
|
||||
- Device returns to startup/menu when a host app opens serial
|
||||
- Browser/CLI connect appears to "knock" signer out of ready screen
|
||||
|
||||
Field-proven mitigation:
|
||||
|
||||
- Add a **10uF capacitor between EN and GND** on the CYD board
|
||||
- A practical solder point is across the board reset path (e.g. reset button EN/GND pads)
|
||||
- For polarized electrolytic caps: **negative leg to GND**, positive to EN
|
||||
|
||||
Expected result after this hardware mod:
|
||||
|
||||
- Serial open no longer triggers unwanted reset in normal host workflows
|
||||
- Signer remains in ready state across browser/CLI connect events
|
||||
|
||||
Notes:
|
||||
|
||||
- Typical working range is ~4.7uF to 22uF; 10uF is recommended
|
||||
- Keep leads short for best stability
|
||||
- Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually
|
||||
|
||||
## Post-quantum crypto support (Phase 7)
|
||||
|
||||
Both firmware targets (`feather_s3_tft` and `cyd_esp32_2432s028`) now include
|
||||
the three NIST-standardized post-quantum algorithms alongside the existing
|
||||
secp256k1 (Nostr) and new ed25519/x25519 classical algorithms:
|
||||
|
||||
| Algorithm | Standard | Purpose | Pub key | Priv key | Sig/Ct |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | — | Nostr (existing) | 32 B | 32 B | 64 B |
|
||||
| ed25519 | RFC 8032 | SSH signatures | 32 B | 32 B | 64 B |
|
||||
| x25519 | RFC 7748 | Key agreement (age) | 32 B | 32 B | — |
|
||||
| ML-DSA-65 | FIPS 204 | PQ signatures | 1952 B | 4032 B | 3309 B |
|
||||
| SLH-DSA-128s | FIPS 205 | PQ hash-based sigs | 32 B | 64 B | 7856 B |
|
||||
| ML-KEM-768 | FIPS 203 | PQ key encapsulation | 1184 B | 2400 B | 1088 B |
|
||||
|
||||
### mbedtls backend (vs OpenSSL on host)
|
||||
|
||||
The host build uses OpenSSL EVP for SHA-256, SHA-512, SHA3-256, SHA3-512,
|
||||
SHAKE-128, and SHAKE-256. On ESP32, OpenSSL is not available. Instead, the
|
||||
firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypto_backend.h`](../resources/pqclean/common/crypto_backend.h))
|
||||
with two implementations:
|
||||
|
||||
- [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP)
|
||||
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls + vendored Keccak)
|
||||
|
||||
The mbedtls backend uses:
|
||||
- `mbedtls_sha256()` for SHA-256 (ESP32 hardware accelerated where available)
|
||||
- `mbedtls_sha512()` for SHA-512 (ESP32 hardware accelerated where available)
|
||||
- A **self-contained Keccak-f[1600]** implementation (FIPS 202, public domain)
|
||||
for SHA3-256, SHA3-512, SHAKE-128, and SHAKE-256. This is vendored directly
|
||||
in `crypto_backend_mbedtls.c` because ESP-IDF v5.x mbedtls does not expose
|
||||
SHAKE (and SHA3 is only available when `CONFIG_MBEDTLS_SHA3_C` is set) through
|
||||
the `mbedtls_md` API. Carrying the Keccak core avoids any mbedtls config
|
||||
dependency for the PQ algorithms.
|
||||
|
||||
### ed25519 / x25519 via PSA crypto
|
||||
|
||||
ESP-IDF v5.x mbedtls removed the low-level `mbedtls_ed25519_*` functions. The
|
||||
firmware uses the **PSA Crypto API** for ed25519 sign/verify/key-derivation and
|
||||
x25519 key derivation + ECDH. Enable PSA in `sdkconfig.defaults`:
|
||||
|
||||
```
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
|
||||
```
|
||||
|
||||
### No SHA3/SHAKE menuconfig requirement
|
||||
|
||||
Because SHA3/SHAKE are provided by the vendored Keccak core (not mbedtls), you
|
||||
do **not** need to enable `CONFIG_MBEDTLS_SHA3_C` or any SHAKE config. The PQ
|
||||
algorithms build and run with the default mbedtls configuration.
|
||||
|
||||
### PQClean component
|
||||
|
||||
The PQClean algorithm code is compiled as an ESP-IDF component at
|
||||
`components/pqclean/`. The component's `CMakeLists.txt` references the shared
|
||||
source files in [`resources/pqclean/`](../resources/pqclean/) via relative
|
||||
paths, so there is a single source of truth for both host and firmware builds.
|
||||
|
||||
The component includes:
|
||||
- ML-DSA-65: `sign.c`, `poly.c`, `ntt.c`
|
||||
- SLH-DSA-128s: `sign.c`, `fors.c`, `wots.c`, `hash.c`, `thash.c`, `address.c`, `utils.c`
|
||||
- ML-KEM-768: `kem.c`, `indcpa.c`, `poly.c`, `ntt.c`, `cbd.c`, `reduce.c`, `symmetric.c`, `verify.c`
|
||||
- Common: `fips202.c`, `sha2.c`, `crypto_backend_mbedtls.c`
|
||||
- Firmware DRBG: `pq_drbg_firmware.c`, `randombytes_mbedtls.c`
|
||||
|
||||
### Flash usage estimates
|
||||
|
||||
| Algorithm | Code size (approx) |
|
||||
|---|---|
|
||||
| ML-DSA-65 | ~150 KB |
|
||||
| SLH-DSA-128s | ~80 KB |
|
||||
| ML-KEM-768 | ~120 KB |
|
||||
| Total PQ code | ~350 KB |
|
||||
|
||||
The ESP32-S3 (Feather S3 TFT) has 8 MB flash and the ESP32 (CYD) has 4 MB
|
||||
flash. The PQ code fits comfortably in both, but partition sizes may need
|
||||
adjustment if the total app image exceeds the default partition.
|
||||
|
||||
### RAM usage notes
|
||||
|
||||
PQ key buffers are large compared to classical ECC keys:
|
||||
|
||||
| Buffer | Size |
|
||||
|---|---|
|
||||
| ML-DSA-65 private key | 4032 bytes |
|
||||
| ML-DSA-65 public key | 1952 bytes |
|
||||
| ML-DSA-65 signature | 3309 bytes |
|
||||
| SLH-DSA-128s signature | 7856 bytes |
|
||||
| ML-KEM-768 private key | 2400 bytes |
|
||||
| ML-KEM-768 public key | 1184 bytes |
|
||||
| ML-KEM-768 ciphertext | 1088 bytes |
|
||||
|
||||
The ESP32 has ~320 KB available heap (after WiFi/BT are disabled). These
|
||||
buffers **must not be stack-allocated** — the default task stack is 8 KB.
|
||||
Use `malloc()` or static buffers. The firmware derives PQ keys **on demand**
|
||||
(not all at startup) to keep peak RAM usage low.
|
||||
|
||||
### SLH-DSA-128s signing latency warning
|
||||
|
||||
SLH-DSA-128s (SPHINCS+-128s) is a hash-based signature scheme with a deep
|
||||
hypertree structure (7 layers of WOTS+ + Merkle trees). On the ESP32-S3
|
||||
(240 MHz dual-core), expect:
|
||||
|
||||
- **Key generation**: 5–30 seconds
|
||||
- **Signing**: 5–30 seconds
|
||||
- **Verification**: 0.5–2 seconds
|
||||
|
||||
This is inherent to the algorithm — it trades computation for minimal trust
|
||||
assumptions (only SHA-256). The firmware logs a warning before SLH-DSA-128s
|
||||
keygen/signing. Users should choose whether to use SLH-DSA-128s per-role
|
||||
based on their latency tolerance. ML-DSA-65 is much faster (~100 ms for
|
||||
signing on ESP32-S3) and is the recommended PQ signature algorithm for
|
||||
interactive use.
|
||||
|
||||
### Derivation paths
|
||||
|
||||
All algorithms derive from the mnemonic using BIP-32/HMAC-SHA512 with
|
||||
SLIP-0010 all-hardened derivation for ed25519/x25519/PQ:
|
||||
|
||||
| Algorithm | Path | Notes |
|
||||
|---|---|---|
|
||||
| secp256k1 (Nostr) | `m/44'/1237'/<n>'/0/0` | NIP-06, existing |
|
||||
| ed25519 (SSH) | `m/44'/102001'/<n>'/0'/0'` | SLIP-0010 |
|
||||
| x25519 (age) | `m/44'/102002'/<n>'/0'/0'` | SLIP-0010 |
|
||||
| ML-DSA-65 | `m/44'/102003'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
| SLH-DSA-128s | `m/44'/102004'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
| ML-KEM-768 | `m/44'/102005'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
|
||||
The PQ derivation produces a 32-byte seed that feeds a deterministic
|
||||
SHAKE-256 DRBG ([`pq_drbg_firmware.c`](feather_s3_tft/components/pqclean/pq_drbg_firmware.c)),
|
||||
which replaces PQClean's `randombytes()` during keygen. This gives
|
||||
deterministic, mnemonic-recoverable PQ keys — same mnemonic, same key pair.
|
||||
|
||||
### Firmware API
|
||||
|
||||
The firmware exposes PQ operations via [`pq_crypto_firmware.h`](feather_s3_tft/main/pq_crypto_firmware.h):
|
||||
|
||||
```c
|
||||
/* Key generation (deterministic from mnemonic-derived seed) */
|
||||
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* Signing / verification */
|
||||
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen, ...);
|
||||
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen, ...);
|
||||
|
||||
/* KEM encaps / decaps */
|
||||
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
||||
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
||||
```
|
||||
|
||||
Key derivation from the mnemonic seed is via [`key_derivation.h`](feather_s3_tft/main/key_derivation.h):
|
||||
|
||||
```c
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
```
|
||||
|
||||
81
firmware/ble_wearable_signer/README.md
Normal file
81
firmware/ble_wearable_signer/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# n_signer BLE Wearable Signer
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
A small, battery-powered wearable hardware signer that communicates with a host
|
||||
over **Bluetooth Low Energy (BLE)**. The host sends JSON-RPC requests over a
|
||||
BLE GATT characteristic; the signer shows an approval prompt on a tiny display;
|
||||
the user taps a button to approve; the signed response goes back over BLE.
|
||||
|
||||
## Concept
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: phone/laptop<br/>n_signer client] -->|BLE GATT| Signer[Wearable signer<br/>nRF52840 + OLED]
|
||||
Signer -->|approve/deny button| User[User]
|
||||
Signer -->|BLE GATT response| Host
|
||||
```
|
||||
|
||||
The signer speaks the same algorithm-based API as the host and the CYD/Teensy
|
||||
firmware ([`README.md`](../../README.md) §4). The auth envelope (kind 27235)
|
||||
protects the BLE wire — even if BLE is sniffed, an attacker can't forge
|
||||
requests without the caller's secp256k1 private key.
|
||||
|
||||
## Why BLE
|
||||
|
||||
- **Wearable form factor** — always with you (wristband, pendant, card)
|
||||
- **No physical connection** — no USB cable, no host-side driver, no dongle
|
||||
- **Universal host support** — phones, laptops, tablets all have BT
|
||||
- **Low power** — nRF52840 draws ~5 mA active, ~1 µA sleep
|
||||
|
||||
## Hardware (preliminary)
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **nRF52840** (Nordic) | Cortex-M4 @ 64 MHz, 1 MB flash, 256 KB RAM, BT 5.0, hardware AES/ECC, USB device, NFC-A. ~$5-8. |
|
||||
| Display | 0.96" or 1.3" SSD1306 OLED (I2C) or 1.02" e-paper | Small is fine — only shows "approve kind 1 from <caller>?" |
|
||||
| Input | 2-3 tactile buttons (approve/deny/back) | No touch at this size |
|
||||
| Power | 200 mAh coin cell or small LiPo | Weeks of battery life |
|
||||
| Mnemonic entry | Buttons (scroll words), NFC from phone, or generate-on-device | The hard UX problem |
|
||||
|
||||
## Security considerations
|
||||
|
||||
- **BT stack attack surface:** BLE has a large stack (pairing, GATT, L2CAP, SMP).
|
||||
A stack bug could allow code execution. Mitigations: use Nordic's audited
|
||||
SoftDevice, disable unnecessary services, require LE Secure Connections pairing.
|
||||
- **Radio range (~10 m):** an attacker in the same room could potentially
|
||||
interact with the signer. The auth envelope + approval prompt protect against
|
||||
this, but the radio is omnidirectional.
|
||||
- **Pairing UX:** BT pairing can be frustrating. LE Secure Connections (Numeric
|
||||
Comparison) is the most secure and user-friendly pairing method.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Mnemonic entry on a tiny screen:** scroll through 2048 BIP-39 words with
|
||||
up/down buttons (like Coldcard)? Load via NFC from a phone? Generate on-device
|
||||
and display for the user to write down?
|
||||
- **PQ crypto on nRF52840:** 256 KB RAM is enough for ML-DSA-65 (~6 KB heap)
|
||||
but SLH-DSA-128s is heavy. May need to limit the PQ algorithm set or stream
|
||||
the keygen.
|
||||
- **Display choice:** OLED (fast refresh, high power) vs e-paper (slow refresh,
|
||||
zero power when static, persistent display).
|
||||
- **Form factor:** wristband? pendant? card? What's the target use case —
|
||||
daily signing, emergency key access, or a backup signer?
|
||||
|
||||
## Comparison to the IR air-gap signer
|
||||
|
||||
| | BLE wearable | IR air-gap |
|
||||
|---|---|---|
|
||||
| Air-gap | Medium (radio, ~10 m, omnidirectional) | High (light, line-of-sight, ~1 m) |
|
||||
| Attack surface | Large (BT stack) | Small (no BT, dumb dongle) |
|
||||
| Host compatibility | Universal (phones, laptops) | Requires USB dongle |
|
||||
| Form factor | Wearable | Handheld (point at dongle) |
|
||||
| Throughput | ~250 KB/s (BLE 5) | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) |
|
||||
| Novelty | Conventional | Novel (no hardware wallet uses IR) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide on the MCU (nRF52840 vs RP2040+BT-module)
|
||||
- Decide on mnemonic entry method
|
||||
- Decide on display (OLED vs e-paper)
|
||||
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
6
firmware/cyd_esp32_2432s028/CMakeLists.txt
Normal file
6
firmware/cyd_esp32_2432s028/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(EXTRA_COMPONENT_DIRS "../feather_s3_tft/components")
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(cyd_esp32_2432s028)
|
||||
175
firmware/cyd_esp32_2432s028/README.md
Normal file
175
firmware/cyd_esp32_2432s028/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# n_signer CYD Firmware (ESP32-2432S028)
|
||||
|
||||
The **Cheap Yellow Display** (ESP32-2432S028) is a $15 ESP32-WROOM-32 board with
|
||||
a 2.8" 320×240 ILI9341 resistive-touch display, CH340 USB-UART bridge, and a
|
||||
Micro SD card slot. This firmware turns it into a hardware n_signer that speaks
|
||||
the same algorithm-based API as the host ([`README.md`](../../README.md) §4).
|
||||
|
||||
**Firmware version:** 0.0.2 (algorithm-based API)
|
||||
|
||||
## Hardware summary
|
||||
|
||||
| Concern | Value |
|
||||
|---|---|
|
||||
| MCU | ESP32-WROOM-32 (classic, dual-core Xtensa, 512 KB SRAM, no PSRAM) |
|
||||
| USB-UART | CH340 (`1a86:7523`) → `/dev/ttyUSB0` |
|
||||
| Flash | 4 MB |
|
||||
| Display | 2.8" 320×240 ILI9341 (HSPI: DC=IO2, CS=IO15, SCK=IO14, MOSI=IO13, MISO=IO12, BL=IO21) |
|
||||
| Touch | XPT2046 resistive (bit-banged SPI: CLK=IO25, MOSI=IO32, CS=IO33, MISO=IO39, IRQ=IO36) |
|
||||
| SD card | Micro SD, VSPI (CS=IO5, SCK=IO18, MISO=IO19, MOSI=IO23) |
|
||||
| RGB LED | R=IO4, G=IO16, B=IO17 (active LOW) |
|
||||
| LDR | IO34 |
|
||||
| Speaker | IO26 (DAC) |
|
||||
| BOOT button | IO0 |
|
||||
| GUI | LVGL 8.3 |
|
||||
|
||||
For the full pin map, connectors (P1/P3/CN1), and add-ons, see the upstream
|
||||
hardware docs copied to [`docs/`](docs/) — especially
|
||||
[`docs/PINS.md`](docs/PINS.md) and [`docs/SETUP.md`](docs/SETUP.md).
|
||||
|
||||
## SD card — size limits and OTP pad storage
|
||||
|
||||
The CYD's Micro SD slot is wired to VSPI (IO5/18/19/23). ESP-IDF drives it via
|
||||
the SDSPI host + FATFS filesystem. The proven bring-up example is
|
||||
`07_sd_card` in the `esp32_playground/cyb-esp32-2432s028/` workspace.
|
||||
|
||||
**Size limits:**
|
||||
|
||||
- **SDSC (≤ 2 GB):** supported.
|
||||
- **SDHC (2 GB – 32 GB):** supported — this is the recommended range. The
|
||||
Makerfabs CYD ships with a 16 GB card, which works.
|
||||
- **SDXC (> 32 GB):** **not supported** out of the box. SDXC cards ship
|
||||
formatted as exFAT, and ESP-IDF's FATFS does not include exFAT. An SDXC card
|
||||
reformatted to FAT32 will work up to 32 GB; beyond that, FAT32's 32 GB limit
|
||||
applies. For OTP pad storage, 32 GB is vastly more than enough (see below).
|
||||
|
||||
**Recommendation:** use any **SDHC card from 4–32 GB** formatted **FAT32**.
|
||||
|
||||
### Using the SD card for the OTP pad
|
||||
|
||||
The current v0.0.2 firmware derives the OTP pad from the mnemonic seed via
|
||||
HKDF-SHA256 into a 1024-byte in-RAM pad (no SD card required). This keeps the
|
||||
wire contract identical to the host's `encrypt`/`decrypt` (algorithm:"otp")
|
||||
verbs but limits the pad to 1024 bytes per session.
|
||||
|
||||
To hold a **large OTP pad** (the original n_signer host design binds a pad file
|
||||
from `--otp-pad-dir`), the SD card is the right storage. The plan:
|
||||
|
||||
1. Format the SD card as FAT32.
|
||||
2. Place a pad file (e.g. `nsigner.pad`) on it — any size up to the card's free
|
||||
space. A 1 GB pad gives ~1 billion one-time-pad bytes before exhaustion.
|
||||
3. The firmware mounts the SD card at boot via `esp_vfs_fat_sdmmc_mount()` on
|
||||
the SDSPI host, opens the pad file, and reads pad bytes on demand into a
|
||||
small ring buffer, advancing a persistent offset (stored in a small
|
||||
`nsigner.offset` file on the SD so the offset survives power cycles).
|
||||
4. The `encrypt`/`decrypt` verbs XOR against the SD-backed pad instead of the
|
||||
HKDF-derived in-RAM pad.
|
||||
|
||||
This is a planned enhancement (see [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md)
|
||||
§13 — the current implementation uses the mnemonic-derived pad as the embedded
|
||||
fallback). The SD card slot is confirmed working and the pin map is in
|
||||
[`docs/PINS.md`](docs/PINS.md).
|
||||
|
||||
**Note on simultaneous display + touch + SD:** The CYD's display (HSPI), touch
|
||||
(bit-banged), and SD (VSPI) use three different SPI buses. All three can run at
|
||||
the same time — the touch is bit-banged precisely so it doesn't contend with
|
||||
the other two hardware SPI buses (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)).
|
||||
|
||||
## Building and flashing
|
||||
|
||||
Requires ESP-IDF v5.x (tested with v5.4.2). The classic ESP32 target uses the
|
||||
`xtensa-esp-elf` unified toolchain.
|
||||
|
||||
```bash
|
||||
source /home/user/esp/esp-idf/export.sh
|
||||
cd firmware/cyd_esp32_2432s028
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash
|
||||
```
|
||||
|
||||
If flashing fails with `Wrong boot mode detected (0x13)`, see the serial-reset
|
||||
hardware note below.
|
||||
|
||||
## Validation — Web Serial
|
||||
|
||||
The CYD has no native USB; the CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html):
|
||||
|
||||
1. Open [`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html) in Chrome/Edge.
|
||||
2. Click **Connect Web Serial**, select the CH340 port (`1a86:7523`).
|
||||
3. On the CYD touchscreen, enter or generate a mnemonic to reach the "ready" state.
|
||||
4. Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp).
|
||||
|
||||
## API
|
||||
|
||||
The CYD firmware speaks the same algorithm-based API as the host n_signer
|
||||
([`README.md`](../../README.md) §4). Supported verbs:
|
||||
|
||||
| Verb | Algorithms |
|
||||
|---|---|
|
||||
| `get_public_key` | secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768 |
|
||||
| `sign` / `verify` | secp256k1 (schnorr/ecdsa), ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 (HMAC-SHA256) |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06, single-threaded PoW) |
|
||||
| `nostr_nip04_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
| `nostr_nip44_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
|
||||
All requests require an auth envelope (kind 27235). The `key_id` in every
|
||||
structured result is the first 16 hex characters of the public key, matching
|
||||
the host. Invalid `(verb, algorithm)` pairs return error `1010`.
|
||||
|
||||
### Embedded-specific notes
|
||||
|
||||
- **OTP pad:** derived from the mnemonic seed (HKDF-SHA256, 1024 bytes) in
|
||||
v0.0.2. The offset advances monotonically and is reported in every
|
||||
`encrypt`/`decrypt` response. SD-card-backed pad is a planned enhancement
|
||||
(see above).
|
||||
- **`nostr_mine_event`:** single-threaded, hard 30 s default timeout, shows a
|
||||
"mining…" screen. Keep difficulty low (≤ 8) on ESP32.
|
||||
- **SLH-DSA-128s:** keygen and signing take 5–30 s. The UI shows a "deriving
|
||||
key…" / "signing…" indicator. ML-DSA-65 is much faster (~100 ms) and is the
|
||||
recommended PQ signature algorithm for interactive use.
|
||||
|
||||
## Crypto backend
|
||||
|
||||
- **SHA-256 / SHA-512:** mbedtls (ESP32 hardware accelerated).
|
||||
- **SHA3 / SHAKE-128 / SHAKE-256:** vendored Keccak-f[1600] (FIPS 202) in
|
||||
[`resources/pqclean/common/crypto_backend_mbedtls.c`](../../resources/pqclean/common/crypto_backend_mbedtls.c).
|
||||
No `CONFIG_MBEDTLS_SHA3_C` or SHAKE menuconfig dependency.
|
||||
- **ed25519 / x25519:** PSA Crypto API (`psa_import_key`, `psa_sign_message`,
|
||||
`psa_raw_key_agreement`, etc.) — ESP-IDF v5.x mbedtls removed the
|
||||
`mbedtls_ed25519_*` functions. Requires `CONFIG_MBEDTLS_PSA_CRYPTO_C=y`
|
||||
(set in [`sdkconfig.defaults`](sdkconfig.defaults)).
|
||||
- **secp256k1:** the vendored secp256k1 component (schnorr + ECDSA).
|
||||
- **PQ (ML-DSA-65, SLH-DSA-128s, ML-KEM-768):** PQClean via the
|
||||
[`components/pqclean/`](components/pqclean/) component.
|
||||
|
||||
## Serial-reset hardware note (CH340 auto-reset)
|
||||
|
||||
Opening `/dev/ttyUSB0` can reset the ESP32 because the CH340's DTR/RTS lines
|
||||
are wired into the ESP32 auto-reset circuit. Symptoms: the device returns to
|
||||
the startup menu when a host app opens the serial port.
|
||||
|
||||
**Mitigation:** add a **10 µF capacitor between EN and GND** on the CYD board
|
||||
(negative leg to GND). Typical working range is 4.7–22 µF. This also fixes the
|
||||
`Wrong boot mode detected (0x13)` flashing error. See
|
||||
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) and the
|
||||
[`firmware/README.md`](../README.md) CYD section for details.
|
||||
|
||||
## Reference documentation
|
||||
|
||||
- [`docs/`](docs/) — upstream CYD hardware docs (PINS, SETUP, TROUBLESHOOTING, ADDONS, etc.)
|
||||
- [`plans/cyd_signer_port.md`](../../plans/cyd_signer_port.md) — original port plan (hardware comparison, architecture, UI flow)
|
||||
- [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md) — v0.0.2 API upgrade plan
|
||||
- [`firmware/README.md`](../README.md) — shared firmware README (PQ crypto, mbedtls backend, feather target)
|
||||
- [`README.md`](../../README.md) §4 — the authoritative n_signer API reference
|
||||
@@ -0,0 +1,60 @@
|
||||
# CMakeLists.txt — ESP-IDF component for PQClean post-quantum algorithms.
|
||||
#
|
||||
# Compiles the three PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
|
||||
# from the shared resources/pqclean/ source tree, using the mbedtls
|
||||
# crypto backend (crypto_backend_mbedtls.c) for SHA-2/SHA3/SHAKE.
|
||||
#
|
||||
# The source files are referenced via relative paths back to the shared
|
||||
# resources/pqclean/ directory so there is a single source of truth.
|
||||
#
|
||||
# mbedtls requirements:
|
||||
# CONFIG_MBEDTLS_SHA3_C=y (for SHA3-256, SHA3-512)
|
||||
# CONFIG_MBEDTLS_SHAKE_C=y (for SHAKE-128, SHAKE-256)
|
||||
# Enable these in menuconfig under Component config -> mbedTLS ->
|
||||
# Hash functions -> SHA-3 and SHAKE.
|
||||
|
||||
set(PQCLEAN_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../resources/pqclean")
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/fors.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/wots.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/hash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/thash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/address.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/utils.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/kem.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/indcpa.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/cbd.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/reduce.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/symmetric.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/verify.c"
|
||||
"${PQCLEAN_ROOT}/common/fips202.c"
|
||||
"${PQCLEAN_ROOT}/common/sha2.c"
|
||||
"${PQCLEAN_ROOT}/common/crypto_backend_mbedtls.c"
|
||||
"randombytes_mbedtls.c"
|
||||
"pq_drbg_firmware.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
"${PQCLEAN_ROOT}/common"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768"
|
||||
REQUIRES
|
||||
mbedtls
|
||||
)
|
||||
|
||||
# Suppress warnings from the PQClean code (it uses C99 patterns that
|
||||
# trigger -Wextra warnings under ESP-IDF's default flags).
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
-Wno-unused-parameter
|
||||
-Wno-sign-compare
|
||||
-Wno-unused-variable
|
||||
-Wno-unused-but-set-variable
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_dsa_65_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/ml-dsa-65/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_kem_768_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_kem/ml-kem-768/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
/* pqclean.h — Umbrella include for the ESP32 firmware PQClean component.
|
||||
*
|
||||
* Exposes the three post-quantum algorithms (ML-DSA-65, SLH-DSA-128s,
|
||||
* ML-KEM-768) and the deterministic DRBG used for mnemonic-recoverable
|
||||
* key generation.
|
||||
*
|
||||
* On ESP32 the underlying hash/SHAKE primitives are provided by the
|
||||
* mbedtls backend (crypto_backend_mbedtls.c) instead of OpenSSL.
|
||||
*/
|
||||
#ifndef FIRMWARE_PQCLEAN_H
|
||||
#define FIRMWARE_PQCLEAN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- ML-DSA-65 (FIPS 204, lattice signatures) --- */
|
||||
#include "ml_dsa_65_api.h"
|
||||
|
||||
/* --- SLH-DSA-128s (FIPS 205, hash-based signatures) --- */
|
||||
#include "slh_dsa_128s_api.h"
|
||||
|
||||
/* --- ML-KEM-768 (FIPS 203, lattice KEM) --- */
|
||||
#include "ml_kem_768_api.h"
|
||||
|
||||
/* --- Deterministic DRBG (replaces randombytes() for keygen) --- */
|
||||
/* Initializes the DRBG with a 32-byte mnemonic-derived seed. Subsequent
|
||||
* randombytes() calls will produce a deterministic byte stream. */
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
|
||||
|
||||
/* Zeroizes the DRBG state (call after keygen to wipe sensitive material). */
|
||||
void pq_drbg_zeroize(void);
|
||||
|
||||
/* randombytes() — called by the PQClean algorithm code.
|
||||
* On firmware this is provided by randombytes_mbedtls.c (deterministic DRBG
|
||||
* for keygen, or mbedtls_ctr_drbg for real randomness during encaps). */
|
||||
int randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
#endif /* FIRMWARE_PQCLEAN_H */
|
||||
@@ -0,0 +1,5 @@
|
||||
/* slh_dsa_128s_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#define FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/slh-dsa-128s/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
/* pq_drbg_firmware.c — Deterministic PRNG for PQ key generation on ESP32.
|
||||
*
|
||||
* Same algorithm as the host's src/pq_drbg.c but uses the crypto backend
|
||||
* abstraction (which resolves to mbedtls on ESP32) for SHAKE-256 instead
|
||||
* of OpenSSL EVP. This allows deterministic PQ key generation from a
|
||||
* mnemonic-derived seed: same seed -> same randombytes output sequence.
|
||||
*
|
||||
* The PRNG: SHAKE-256(seed || counter) produces a stream of pseudo-random
|
||||
* bytes. The counter is a 64-bit little-endian integer that increments
|
||||
* each time we need more output.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "crypto_backend.h"
|
||||
|
||||
/* --- DRBG state --- */
|
||||
|
||||
static unsigned char g_seed[32];
|
||||
static int g_seed_len = 0;
|
||||
static uint64_t g_counter = 0;
|
||||
static unsigned char g_buffer[168]; /* SHAKE-256 rate = 136, 168 for safety */
|
||||
static size_t g_buffer_pos = sizeof(g_buffer);
|
||||
static int g_initialized = 0;
|
||||
|
||||
/* --- internal: squeeze more bytes from SHAKE-256 --- */
|
||||
|
||||
static void drbg_refill(void) {
|
||||
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
|
||||
|
||||
memcpy(seed_block, g_seed, (size_t)g_seed_len);
|
||||
seed_block[g_seed_len + 0] = (unsigned char)(g_counter & 0xFF);
|
||||
seed_block[g_seed_len + 1] = (unsigned char)((g_counter >> 8) & 0xFF);
|
||||
seed_block[g_seed_len + 2] = (unsigned char)((g_counter >> 16) & 0xFF);
|
||||
seed_block[g_seed_len + 3] = (unsigned char)((g_counter >> 24) & 0xFF);
|
||||
seed_block[g_seed_len + 4] = (unsigned char)((g_counter >> 32) & 0xFF);
|
||||
seed_block[g_seed_len + 5] = (unsigned char)((g_counter >> 40) & 0xFF);
|
||||
seed_block[g_seed_len + 6] = (unsigned char)((g_counter >> 48) & 0xFF);
|
||||
seed_block[g_seed_len + 7] = (unsigned char)((g_counter >> 56) & 0xFF);
|
||||
|
||||
crypto_backend_shake256(seed_block, (size_t)g_seed_len + 8,
|
||||
g_buffer, sizeof(g_buffer));
|
||||
|
||||
g_counter++;
|
||||
g_buffer_pos = 0;
|
||||
}
|
||||
|
||||
/* --- public API --- */
|
||||
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len) {
|
||||
if (seed == NULL || seed_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_seed, 0, sizeof(g_seed));
|
||||
if (seed_len > sizeof(g_seed)) {
|
||||
seed_len = sizeof(g_seed);
|
||||
}
|
||||
memcpy(g_seed, seed, seed_len);
|
||||
g_seed_len = (int)sizeof(g_seed); /* always use 32-byte seed (zero-padded) */
|
||||
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 1;
|
||||
}
|
||||
|
||||
void pq_drbg_zeroize(void) {
|
||||
crypto_backend_cleanse(g_seed, sizeof(g_seed));
|
||||
crypto_backend_cleanse(g_buffer, sizeof(g_buffer));
|
||||
g_seed_len = 0;
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 0;
|
||||
}
|
||||
|
||||
/* Returns 1 if the DRBG has been initialized (keygen mode), 0 otherwise.
|
||||
* Used by randombytes_mbedtls.c to decide between deterministic DRBG and
|
||||
* hardware RNG. */
|
||||
int pq_drbg_is_initialized(void) {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
/* pq_drbg_randombytes is called by randombytes() below. */
|
||||
int pq_drbg_randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || !g_initialized) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
size_t avail;
|
||||
size_t to_copy;
|
||||
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
drbg_refill();
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
return -1; /* refill failed */
|
||||
}
|
||||
}
|
||||
|
||||
avail = sizeof(g_buffer) - g_buffer_pos;
|
||||
to_copy = (len < avail) ? len : avail;
|
||||
memcpy(buf, g_buffer + g_buffer_pos, to_copy);
|
||||
g_buffer_pos += to_copy;
|
||||
buf += to_copy;
|
||||
len -= to_copy;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* randombytes_mbedtls.c — randombytes() implementation for ESP32 firmware.
|
||||
*
|
||||
* PQClean's algorithm code calls randombytes() for:
|
||||
* 1. Key generation (keygen) — must be deterministic from the mnemonic
|
||||
* seed so keys are recoverable. The DRBG is initialized via
|
||||
* pq_drbg_init() before keygen, so randombytes() draws from the
|
||||
* deterministic stream.
|
||||
* 2. Encapsulation (ML-KEM enc) — needs real cryptographic randomness.
|
||||
* When the DRBG is NOT initialized, randombytes() falls back to
|
||||
* esp_fill_random() which uses the ESP32 hardware RNG.
|
||||
*
|
||||
* This dual-mode behavior matches the host build (src/pq_drbg.c) where
|
||||
* the DRBG is initialized for keygen and randombytes() returns -1 if
|
||||
* called without initialization. On firmware we allow the fallback to
|
||||
* hardware RNG for encaps, which is the correct behavior.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "esp_random.h"
|
||||
|
||||
/* Defined in pq_drbg_firmware.c */
|
||||
extern int pq_drbg_randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
/* Check if the DRBG is initialized (declared in pq_drbg_firmware.c).
|
||||
* We use a helper to avoid exposing the static directly. */
|
||||
extern int pq_drbg_is_initialized(void);
|
||||
|
||||
int randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* If the deterministic DRBG is active (keygen mode), use it. */
|
||||
if (pq_drbg_is_initialized()) {
|
||||
return pq_drbg_randombytes(buf, len);
|
||||
}
|
||||
|
||||
/* Otherwise, use the ESP32 hardware RNG for real randomness (encaps). */
|
||||
esp_fill_random(buf, len);
|
||||
return 0;
|
||||
}
|
||||
18
firmware/cyd_esp32_2432s028/dependencies.lock
Normal file
18
firmware/cyd_esp32_2432s028/dependencies.lock
Normal file
@@ -0,0 +1,18 @@
|
||||
dependencies:
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.4.2
|
||||
lvgl/lvgl:
|
||||
component_hash: d7c1ac037ae6e85d94897f807d6e7ba0946a83e720074fc95a4f6241da9f9f53
|
||||
dependencies: []
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 8.4.0
|
||||
direct_dependencies:
|
||||
- idf
|
||||
- lvgl/lvgl
|
||||
manifest_hash: 71303cd3f2b23b31340e007c801196d039bc08fd0f0fb05f09f49f579690eb29
|
||||
target: esp32
|
||||
version: 2.0.0
|
||||
86
firmware/cyd_esp32_2432s028/docs/ADDONS.md
Normal file
86
firmware/cyd_esp32_2432s028/docs/ADDONS.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Add-Ons
|
||||
|
||||
Here is a list of additional hardware add-ons that can add functionality to your CYD
|
||||
|
||||
## SD Card Sniffer
|
||||
|
||||
If you want to use the pins of the SD card for a different purpose, the easiest way to do that is with an "SD card sniffer", which basically plugs into the SD card slot and breaks out the pins. It's particularly useful for SPI devices.
|
||||
|
||||
## Pin-out of the Sniffer board
|
||||
|
||||
| Sniffer Board Label | ESP32 Pin | SPI Use |
|
||||
| ------------------- | --------- | --------- |
|
||||
| DAT2 | - | - |
|
||||
| CD | IO5 | CS |
|
||||
| CMD | IO23 | DI / MOSI |
|
||||
| GND | GND | - |
|
||||
| VCC | 3.3V | - |
|
||||
| CLK | IO18 | SCLK |
|
||||
| DAT0 | IO19 | DO / MISO |
|
||||
| DAT1 | - | - |
|
||||
|
||||
### Links
|
||||
|
||||
- [Micro SD Card Sniffer - Aliexpress\*](https://s.click.aliexpress.com/e/_Ddwcy9h)
|
||||
|
||||
## Nintendo Wii Nunchuck
|
||||
|
||||
A Nunchuck controller from a Nintendo Wii is a great input device for CYD projects as they are inexpensive and, since they use i2c for communication, they only require 2 GPIO pins to connect them up.
|
||||
|
||||
For these two pins you get:
|
||||
|
||||
- An analog stick
|
||||
- 2 Buttons
|
||||
- An accelerometer
|
||||
|
||||
### Hardware Required
|
||||
|
||||
#### Nunchuck controllers
|
||||
|
||||
Official Nintendo ones are generally better (maybe try second-hand options), but third-party ones also work fine.
|
||||
|
||||
- [Amazon.co.uk Search\*](https://amzn.to/3nQrXcE)
|
||||
- [Amazon.com Search\*](https://amzn.to/3nRJTUd)
|
||||
- [Aliexpress (Third Party)\*](https://s.click.aliexpress.com/e/_AaQbXh)
|
||||
|
||||
#### Nunchuck Adaptors
|
||||
|
||||
There are many different options available for these, even the cheap ones from Aliexpress work perfectly.
|
||||
|
||||
- [Aliexpress](https://s.click.aliexpress.com/e/_AEEtc3)
|
||||
- [My Open source one from Oshpark](https://oshpark.com/shared_projects/RcIxSx2D)
|
||||
- [Adafaruit](https://www.adafruit.com/product/4836)
|
||||
|
||||
### Wiring
|
||||
|
||||
The easiest way to wire this up is to use the wire that came with the CYD and the **CN1** JST connector (the one closest to the Micro SD card slot)
|
||||
|
||||
Connect the wire to your breakout board as follows:
|
||||
|
||||
| CYD CN1 | Adapter | Note |
|
||||
| ------- | ----------- | ------------------ |
|
||||
| GND | - (AKA GND) | Black wire for me |
|
||||
| 3.3V | + (AKA 3V) | Red wire for me |
|
||||
| IO22 | d (AKA SDA) | Blue wire for me |
|
||||
| IO27 | c (AKA SCL) | Yellow wire for me |
|
||||
|
||||
Note: I have found pull-ups resistors are not required on SDA and SCL
|
||||
|
||||
### Example
|
||||
|
||||
Check out the [NunchuckTest](/Examples/InputTests/NunchuckTest) example for code how to use it.
|
||||
|
||||
## Speakers
|
||||
|
||||
A speaker can be attached to the display with a 1.25mm JST connector to the connector labeled "SPEAK" (or soldered)
|
||||
|
||||
Check out the [HelloRadio](/Examples/Basics/7-HelloRadio) example for the code on how to use it.
|
||||
|
||||
Most small 8 Ohm speakers should work. Maybe worth adding a 1.25mm JST connector to it to make it easy to add remove.
|
||||
|
||||
### Links
|
||||
|
||||
- [Speaker with 1.25mm JST connector (2pcs) - Aliexpress\*](https://s.click.aliexpress.com/e/_DBOJoh7) - Tested, works right out of the package.
|
||||
- [2pin 1.25mm JST connectors - Aliexpress\*](https://s.click.aliexpress.com/e/_DlbPkWH) - Not purchased by me, but should work
|
||||
|
||||
\* = Affiliate Link - It doesn't cost you any extra but I receive a small portion of the sale.
|
||||
14
firmware/cyd_esp32_2432s028/docs/MEDIA.md
Normal file
14
firmware/cyd_esp32_2432s028/docs/MEDIA.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Media and Mentions
|
||||
|
||||
This page can document any times the CYD project was mentioned somewhere!
|
||||
|
||||
## Videos
|
||||
|
||||
- [Brian Lough (hey, thats me!) - Cheap and Easy to Use ESP32 Screen!](https://www.youtube.com/watch?v=0AVyvwv0agk)
|
||||
- [Talking Sasquach - Don't be Fooled!! This Cheap Yellow Display Can Do a LOT!!](https://youtu.be/PsqMCoCTgTg?feature=shared)
|
||||
- [Teaching Tech - Cheap and easy Klipper touch interface with CYD Klipper](https://youtu.be/R3o0MGYW1ZU?feature=shared)
|
||||
|
||||
## Articles
|
||||
|
||||
- [Hackaday.com - “Cheap Yellow Display” Builds Community Through Hardware](https://hackaday.com/2023/10/28/cheap-yellow-display-builds-community-through-hardware/)
|
||||
- [Hackster.io - Brian Lough Looks to Build a Community Around the Espressif ESP32-Powered "Cheap Yellow Display"](https://www.hackster.io/news/brian-lough-looks-to-build-a-community-around-the-espressif-esp32-powered-cheap-yellow-display-66d23972910d)
|
||||
140
firmware/cyd_esp32_2432s028/docs/PINS.md
Normal file
140
firmware/cyd_esp32_2432s028/docs/PINS.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Pins
|
||||
|
||||
This page talks about the pins on the CYD.
|
||||
|
||||
## Connector types
|
||||
|
||||
The connectors are often called "1.25mm JST" but the correct name is "Molex PicoBlade".
|
||||
Chinese clones are sometimes called "mx1.25".
|
||||
|
||||
|Connector|Type |Note |
|
||||
|--- |--- |---- |
|
||||
|[**P1**](#p1) |4P 1.25mm Molex PicoBlade|Serial |
|
||||
|[**P3**](#p3) |4P 1.25mm Molex PicoBlade|GPIO |
|
||||
|[**P4**](#p4) |2P 1.25mm Molex PicoBlade|Speaker |
|
||||
|[**CN1**](#cn1)|4P 1.25mm Molex PicoBlade|GPIO (I2C) |
|
||||
|
||||
## What pins are available on the CYD?
|
||||
|
||||
There are 3 easily accessible GPIO pins
|
||||
|
||||
|Pin|Location|Note|
|
||||
|---|---|----|
|
||||
|IO35|**P3** Molex PicoBlade connector|Input only pin, no internal pull-ups available|
|
||||
|IO22|**P3** and **CN1** Molex PicoBlade connector||
|
||||
|IO27|**CN1** Molex PicoBlade connector||
|
||||
|
||||
If you need more than that, you need to start taking them from something else. An SD Card sniffer like mentioned in the [Add-ons](/ADDONS.md) is probably the next easiest.
|
||||
|
||||
After that you're probably de-soldering something!
|
||||
|
||||
## Broken Out Pins
|
||||
|
||||
There are three 4P 1.25mm Molex PicoBlade connectors on the board.
|
||||
|
||||
### P3
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO35||Input only pin, no internal pull-ups available|
|
||||
|IO22||Also on the **CN1** connector|
|
||||
|IO21||Used for the TFT Backlight, so not really usable|
|
||||
|
||||
### CN1
|
||||
This is a great candidate for I2C devices
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO22||Also on **P3** connector|
|
||||
|IO27|||
|
||||
|3.3V|||
|
||||
|
||||
### P1
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|VIN|||
|
||||
|IO1(?)|TX|Maybe possible to use as a GPIO?|
|
||||
|IO3(?)|RX|Maybe possible to use as a GPIO?|
|
||||
|GND|||
|
||||
|
||||
|
||||
## Buttons
|
||||
|
||||
The CYD has two buttons, reset and boot.
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO0|BOOT|Can be used as an input in sketches|
|
||||
|
||||
## Speaker
|
||||
|
||||
The speaker connector is a 2P 1.25mm Molex PicoBlade connector that is connected to the amplifier, so not usable as GPIO at the speaker connector
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO26|Connected to amp|`i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN);`|
|
||||
|
||||
## RGB LED
|
||||
|
||||
If your project requires additional pins to what is available elsewhere, this might be a good candidate to sacrifice.
|
||||
|
||||
Note: LEDs are "active low", meaning HIGH == off, LOW == on
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO4|Red LED||
|
||||
|IO16|Green LED||
|
||||
|IO17|Blue LED||
|
||||
|
||||
## SD Card
|
||||
Uses the VSPI
|
||||
Pin names are predefined in SPI.h
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO5|SS||
|
||||
|IO18|SCK||
|
||||
|IO19|MISO||
|
||||
|IO23|MOSI||
|
||||
|
||||
## Touch Screen
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO25|XPT2046_CLK||
|
||||
|IO32|XPT2046_MOSI||
|
||||
|IO33|XPT2046_CS||
|
||||
|IO36|XPT2046_IRQ||
|
||||
|IO39|XPT2046_MISO||
|
||||
|
||||
## LDR (Light Sensor)
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO34|||
|
||||
|
||||
## Display
|
||||
Uses the HSPI
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO2|TFT_RS|AKA: TFT_DC|
|
||||
|IO12|TFT_SDO|AKA: TFT_MISO|
|
||||
|IO13|TFT_SDI|AKA: TFT_MOSI|
|
||||
|IO14|TFT_SCK||
|
||||
|IO15|TFT_CS||
|
||||
|IO21|TFT_BL|Also on P3 connector, for some reason|
|
||||
|
||||
## Test points
|
||||
|Pad|Use|Note|
|
||||
|---|---|----|
|
||||
|S1|GND|near USB-SERIAL|
|
||||
|S2|3.3v|for ESP32|
|
||||
|S3|5v|near USB-SERIAL|
|
||||
|S4|GND|for ESP32|
|
||||
|S5|3.3v|for TFT|
|
||||
|JP0 (pad nearest USB socket)|5v|TFT LDO|
|
||||
|JP0|3.3v|TFT LDO|
|
||||
|JP3 (pad nearest USB socket)|5v|ESP32 LDO|
|
||||
|JP3|3.3v|ESP32 LDO|
|
||||
60
firmware/cyd_esp32_2432s028/docs/PROJECTS.md
Normal file
60
firmware/cyd_esp32_2432s028/docs/PROJECTS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Projects
|
||||
|
||||
Because the CYD is a common platform, it makes it really useful for sharing projects. This page will be a list of projects that are available on the CYD.
|
||||
|
||||
## Disclaimer!
|
||||
|
||||
Projects appearing on here is not necessarily a seal of approval from me, I will not be test each project that gets added, so please install these projects at your own risk!
|
||||
|
||||
## Projects
|
||||
|
||||
| Name | Description | Author | Additional Hardware? | Project Page | WebFlash |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| Spotify DIY Thing | A device for displaying your currently playing Spotify track | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/Spotify-Diy-Thing) | [WebFlash](https://witnessmenow.github.io/Spotify-Diy-Thing/) |
|
||||
| F1 Notifier | Displays and notifies you of the F1 session times(in your local timezone) | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/F1-Arduino-Notifications) | [WebFlash](https://witnessmenow.github.io/F1-Arduino-Notifications/) |
|
||||
| Tetris with Nunchuck | A version of Tetris using a Nintendo wii Nunchuck | [Brian Lough](https://github.com/witnessmenow) | A nunchuck and an adaptor for connecting it | [Code](/Examples/Projects/TetrisWithNunchuck) | |
|
||||
| Galagino | An emulator for some classic arcade games (Galaga, Donkey Kong, Pacman, Digdug and Frogger) | [Till Harbaum](https://github.com/harbaum) | A nunchuck and an adaptor for connecting it. Speaker if you want sound | [Github](https://github.com/harbaum/galagino) | |
|
||||
| ESP32-fluid-simulation | A small fluid simulation with touch input | [Kenny Peng](https://github.com/colonelwatch) | | [Github](https://github.com/colonelwatch/ESP32-fluid-simulation) | |
|
||||
| ESP32-TV | Play Video Files on the ESP32 | [atomic14](https://github.com/atomic14) | Speaker if you want sound and possibly an IR receiver | [Github](https://github.com/atomic14/esp32-tv) | |
|
||||
| xtouch | "The xtouch screen is a revolutionary addition to your BambuLab Printer" | [xperiments-in](https://github.com/xperiments-in) (\#) | | [Github](https://github.com/xperiments-in/xtouch) | [Webflash](https://github.com/xperiments-in/xtouch#online-web-installer) |
|
||||
| CYD-Klipper | An implementation of a wireless Klipper status display on an ESP32 + screen | [Sims](https://github.com/suchmememanyskill) | | [Github](https://github.com/suchmememanyskill/CYD-Klipper) | [Webflash](https://suchmememanyskill.github.io/CYD-Klipper/) |
|
||||
| DRO (for lathe / mill) | A DRO (digital readout) for your lathe or mill | [Alanesq](https://github.com/alanesq) | It uses cheap digital caliper, requires a very basic interface | [Github](https://github.com/alanesq/DRO) | |
|
||||
| ESP32Marauder-CYD | A suite of WiFi/Bluetooth offensive and defensive tools for the ESP32 | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | GPS if you want BT/Wifi wardriving options | [Github](https://github.com/Fr4nkFletcher/ESP32-Marauder-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/Adafruit_WebSerial_ESPTool/) |
|
||||
| NerdMiner_v2 | A project that lets you try to solve a bitcoin block with a small piece of hardware. | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | | [Github](https://github.com/Fr4nkFletcher/NerdMiner_v2-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/NerdMiner_v2-Cheap-Yellow-Display/flash.html) |
|
||||
| Tasmota | Tasmota (with UI) on the CYD | ? (\#) | | [Templates](https://templates.blakadder.com/sunton_ESP32-2432S028.html) | [Webflash](https://tasmota.github.io/install/) |
|
||||
| BAM | A game engine featuring smooth scrolling tile map, sprites in layers with pixel precision on-screen collision detection, intuitive definition of game objects and logic, decent performance, ~30 frames per second on the device | [calint](https://github.com/calint) | | [Github](https://github.com/calint/bam) | |
|
||||
|London Underground Arrivals| A highly configurable application that replicates the train arrivals boards found in [TFL](https://tfl.gov.uk/) stations. All variable data is encoded in a json file that may be updated at any time without the need to recompile the application e.g. the station to be displayed or the time to refresh data from TFL. The source code already supports 2 variants of CYD and, I hope, contains clear instructions how to handle any other variant.| [David Henry](https://github.com/mgaman) | | [Github](https://github.com/mgaman/TFL-tube-arrivals-board-ESP32-TFT-Arduino) |
|
||||
|GitHub-Stats| This Arduino project fetches and displays GitHub repository statistics such as star count, open issues, forks and notifactions on a CYD or via serial communication. Ideal for developers to monitor project metrics in real time.| [ATOMNFT](https://github.com/ATOMNFT) | | [Github](https://github.com/ATOMNFT/ESP32-CYD-Projects/tree/main/GitHub-Stats) | |
|
||||
| Midbar-Firebase-Edition | An advanced password vault that stores the encrypted data in the cloud while keeping the cryptographic keys on the edge! | [Northstrix](https://github.com/Northstrix) | PS/2 keyboard and an optional STM32F103C8T6 (if you want it to emulate the USB keyboard) | [SourceForge](https://sourceforge.net/projects/midbar-firebase-edition/) [Github](https://github.com/Northstrix/Midbar-Firebase-Edition)
|
||||
| Electronic-Shelf-Label-Management-System | A simple device for displaying relevant product information. It gets the encrypted images via UDP. | [Northstrix](https://github.com/Northstrix)| | [SourceForge](https://sourceforge.net/projects/esl-management-system/) [Github](https://github.com/Northstrix/Electronic-Shelf-Label-Management-System)
|
||||
| ESP32-Tetris-With-Nintendo-64-Controller | Tetris for ESP32 with Nintendo 64 controller support | [Northstrix](https://github.com/Northstrix) | Nintendo 64 Controller and Arduino Nano | [SourceForge](https://sourceforge.net/projects/esp32-tetris/) [Github](https://github.com/Northstrix/ESP32-Tetris-With-Nintendo-64-Controller)
|
||||
| Midbar ESP32 CYD | A version of Midbar data vault tweaked specifically for the ESP32 Cheap Yellow Display. | [Northstrix](https://github.com/Northstrix) | PS/2 Keyboard | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd/) [Github](https://github.com/Northstrix/Midbar-ESP32-CYD)
|
||||
| ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase | An ESP32 CYD-based Electronic Shelf Label that makes use of the Google Firebase and AES-256. | [Northstrix](https://github.com/Northstrix) | | [SourceForge](https://sourceforge.net/projects/esp32-cyd-esl-with-firebase/) [Github](https://github.com/Northstrix/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase) | [WebFlash](https://northstrix.github.io/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase/flash.html) </br>!!! Format Flash area designated for SPIFFS with [ESP32 Filesystem Uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin/releases/) after using the WebFlash
|
||||
| Addressable RGB LED Strip Controller (The Lantern Project) | DIY Addressable RGB LED Strip Controller that utilizes the ESP32, ESP8266, and the WS2812 LED Strip. | [Northstrix](https://github.com/Northstrix) | Nintendo Wii Nunchuk, WiiChuck Nunchuck Adapter (PCB Board), ESP8266, 580 Ohm resistor, WS2812 LED Strip | [SourceForge](https://sourceforge.net/projects/the-lantern-project/) [Github](https://github.com/Northstrix/Lantern)
|
||||
| Midbar ESP32 CYD Firebase Edition | A version of Midbar data vault adapted for the ESP32 CYD and WebFlash. It keeps the cryptographic keys in the ESP32 RAM and stores the ciphertexts (encrypted data) in the Google Firebase. | [Northstrix](https://github.com/Northstrix) (Adapted for CYD2USB by [Rovel](https://github.com/Rovel))| PS2 Keyboard, PS2 Port *optional | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd-firebase/) [Github (CYD)](https://github.com/Northstrix/Midbar-ESP32-CYD-Firebase-Edition) [Github (CYD2USB)](https://github.com/Northstrix/Midbar-ESP32-CYD2USB-Firebase-Edition) | [WebFlash (CYD)](https://northstrix.github.io/Midbar-ESP32-CYD-Firebase-Edition/flash) [WebFlash (CYD2USB)](https://northstrix.github.io/Midbar-ESP32-CYD2USB-Firebase-Edition/flash)
|
||||
| cydOS (WIP) | cydOS is a GUI app that is able to manage various aspects of the CYD, like SD browsing and file mangement, on board flashing of .bin files for rapid firmware switching, on board device settings(WIP) | [orlandobianco](https://github.com/orlandobianco) | | [Github]((https://github.com/orlandobianco/cydOS)) | |
|
||||
| ESP32 MFA Authenticator | Turn the CYD into a MFA Authenticator | [AllanOricil](https://github.com/AllanOricil) | | [Github](https://github.com/AllanOricil/esp32-mfa-authenticator) | [Webflash](https://allanoricil.github.io/esp32-mfa-authenticator/)
|
||||
| cydWeatherStation | cyd Weather station | [gustheseventh](https://github.com/gustheseventh) (#) | | [Github](https://github.com/gustheseventh/cyd-Weather-Station) | |
|
||||
| PhilRadio | CYD Wifi Radio project. Re-using an old radio as hardware. Exposing a webserver on local network to configure the radio stations. Persistent storage. | [mogrikid](https://github.com/mogrikid) | Required: A speaker. Recommended: Speaker, potentiometer, 10kohm resistor, female usb port, switch | [Github](https://github.com/mogrikid/PhilRadio)
|
||||
| cydWeeWX | Simple CYD Weather Display for the open source [WeeWX](https://www.weewx.com/) weather station server. | [hcomet](https://hcomet.github.io/) | | [Github](https://github.com/hcomet/cydWeeWX)| [Webflash](https://hcomet.github.io/cydWeeWX/cydWeeWXFlash.html) |
|
||||
| CYD Stream Deck | A customizable touch-based Bluetooth HID controller using CYD. | [gahingwoo](https://github.com/gahingwoo) | | [GitHub](https://github.com/gahingwoo/cyd-stream-deck) | [Webflash](https://gahingwoo.github.io/cyd-stream-deck/webflash/index.html) |
|
||||
| CYD DHT22 Weather Clock | A weather and time display using CYD. | [gahingwoo](https://github.com/gahingwoo) | DHT22 sensor | [GitHub](https://github.com/gahingwoo/cyd-dht22-weather-clock) | |
|
||||
| ESP CYD MCP | Model Context Protocol (MCP) server implementation for the ESP32 CYD | [OfryL](https://github.com/OfryL) | | [Github](https://github.com/OfryL/esp-cyd-mcp) | |
|
||||
| Aura | Smart weather forecast device (OpenMeteo) | [Surrey-Homeware](https://github.com/Surrey-Homeware/) (\#) | [3D printed case](https://makerworld.com/en/models/1382304-aura-smart-weather-forecast-display#profileId-1430951) | [Github](https://github.com/Surrey-Homeware/Aura) | [Webflash](https://surrey-homeware.github.io/aura-installer/) |
|
||||
| SmartEnergyMeter | An ESPHome display for Energy in the house (solar, battery, etc) | [anthony-spruyt](https://github.com/anthony-spruyt) (#) | | [Github](https://github.com/anthony-spruyt/ESPHOME-ESP32_CYD_V2-SmartEnergyMeter) | |
|
||||
| Navi Phone | Relica of the Mobile Phones used int the anime Serial Experments Lain | [Aquafrostbyte](https://github.com/AquaFrostByte) (#) | A Sd card is required, Speaker and Wifi is optional | [Github](https://github.com/AquaFrostByte/Navi-Phone) | |
|
||||
| OASMan | Open-source Air Suspension Management - Worlds first DIY Digital Air suspension controller for your car! | [gopro_2027](https://github.com/gopro2027/) | Ideally you would build the manifold and install it in your car, but if you just want to test the connection you can use an original esp32 dev board and flash the manifold code through platformio. We also have a [3d printable case](https://github.com/gopro2027/ArduinoAirSuspensionController/blob/main/3d%20Prints/other/3.2%20inch%20screen%20case/3.2%20inch%20CYD%20screen%20container%20v19%20-%20gopro_2027's%20design.stl) | [Github](https://github.com/gopro2027/ArduinoAirSuspensionController) | [Webflash](https://oasman.dev/oasman/flash/) |
|
||||
| Sonos Remote Control | Use your Sonos Speakers as Internet Radio with Station Buttons | Florian Lenz | https://github.com/SpringTideSystems | [GitHub](https://github.com/SpringTideSystems/CYD_Sonos-RemoteControl) | |
|
||||
|
||||
(\#) = Project not added by original author
|
||||
|
||||
## Adding a project
|
||||
|
||||
If you have a project that you would like to add, please feel free to add it to the list!
|
||||
|
||||
New projects should be added to bottom of the list.
|
||||
|
||||
Some rules:
|
||||
|
||||
- Project must be open source
|
||||
- Project must be functional - It's ok for it to not be finished, but it should do what it says!
|
||||
114
firmware/cyd_esp32_2432s028/docs/README.md
Normal file
114
firmware/cyd_esp32_2432s028/docs/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# ESP32-Cheap-Yellow-Display
|
||||
|
||||
There is an ESP32 with a built in 320 x 240 2.8" LCD display with a touch screen called the "ESP32-2432S028R", since this doesn't roll of the tongue, I propose it should be renamed the "Cheap Yellow Display" or CYD for short. This display is only about $15 delivered so I think it's really good value.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
The CYD has the following features:
|
||||
|
||||
- ESP32 (With Wifi and Bluetooth)
|
||||
- 320 x 240 LCD Display (2.8")
|
||||
- Touch Screen (Resistive)
|
||||
- USB for powering and programming
|
||||
- SD Card Slot, LED and some additional pins broken out
|
||||
|
||||
## Who is it good for?
|
||||
|
||||
I think it's useful for the following types of people:
|
||||
|
||||
- **People just getting started with working hardware** - as everything is already connected, there is no soldering or additional components required
|
||||
- **People who are familiar with working with hardware, but are lazy** - (like me) Sometimes you just want to build a project without having to assemble any hardware
|
||||
- **People who aren't really looking to learn anything, but just want to build some cool things** - More about this later.
|
||||
|
||||
## What is the purpose of this page?
|
||||
|
||||
So this is pretty nice hardware and a cheap price, but the software instructions/support around it is pretty poor. Just a single link to a zip file on a random website.
|
||||
|
||||
A couple of years ago I released the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity), which is an open source ESP32 board for controlling Matrix panels. I think the main benefit people get out of the work I did on the Trinity is not the hardware, but the documentation, example code and ready to go projects.
|
||||
|
||||
I'm no longer creating hardware products, but I think it would be interesting if we could create the same kind of community around this display, where people can share examples and projects made for this display.
|
||||
|
||||
## How do I know if a display is a CYD?
|
||||

|
||||
|
||||
## Where to buy?
|
||||
|
||||
Buy from wherever works out cheapest for you:
|
||||
|
||||
- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkSpIjB)
|
||||
- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkcmuCh)
|
||||
- [Aliexpress](https://www.aliexpress.com/item/1005004502250619.html)
|
||||
- [Makerfabs](https://www.makerfabs.com/sunton-esp32-2-8-inch-tft-with-touch.html) - Seems to come with a 16GB SD card. Makerfabs also stock my [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity) (NOTE there will be import due in the EU from makerfabs)
|
||||
|
||||
\* = Affiliate Link
|
||||
|
||||
## Getting Started With Your CYD
|
||||
|
||||
For details on how to get started with your CYD, please check out the [Setup and Configuration](/SETUP.md) page
|
||||
|
||||
## Code Examples
|
||||
|
||||
### The Basics
|
||||
|
||||
A collection of examples demonstrating how to use the different features of the CYD, this is a good place to get started. [Check them out here.](/Examples/Basics)
|
||||
|
||||
### Alternative Display Libraries
|
||||
|
||||
The basics examples are based on the TFT_eSPI display library, but the CYD also works with other display libraries too. Here is some example code if you prefer to use an alternative Arduino library. [Check them out here.](/Examples/AlternativeLibraries)
|
||||
|
||||
### ESPHome
|
||||
|
||||
Some examples for using the CYD in ESPHome. [Check them out here.](/Examples/ESPHome)
|
||||
|
||||
## Additional Info and Links
|
||||
|
||||
### Discord
|
||||
|
||||
Join the CYD discussion on [my Discord channel](https://discord.gg/nnezpvq)
|
||||
|
||||
### 3DPrinting
|
||||
|
||||
Some examples of 3D printed stands and cases. [Check them out here.](/3dModels)
|
||||
|
||||
### Pin Information
|
||||
|
||||
[This page](/PINS.md) contains information about what pins are used where, and what ones are free to use.
|
||||
|
||||
### Add-ons
|
||||
|
||||
[This page](/ADDONS.md) contains information about additional hardware add-ons that can add functionality to your CYD
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
[This page](/TROUBLESHOOTING.md) contains information about how to troubleshoot your CYD device
|
||||
|
||||
### Hardware Mods
|
||||
|
||||
[This page](/Mods/README.md) contains information about some hardware mods that can be performed on the CYD to improve or change some of its functionality
|
||||
|
||||
### Media and Video Mentions
|
||||
|
||||
[This page](/MEDIA.md) lists any times the CYD project was mentioned somewhere!
|
||||
|
||||
## License Info
|
||||
|
||||
This project is licensed as MIT as per the [license file](/LICENSE)
|
||||
|
||||
The one exception to this is the [OriginalDocumentation](/OriginalDocumentation/) folder, that I do not have the right to license
|
||||
|
||||
## Other Languages
|
||||
|
||||
Some members of the community have ported some of this information to other languages!
|
||||
|
||||
Please note: I can't gaurantee the accuracy of the translation, how up to date they are or the content on them in general.
|
||||
|
||||
- [French / Française](https://github.com/usini/ESP32-Cheap-Yellow-Display-Documentation-FR)
|
||||
- [German / Deutsch](https://github.com/paelzer/ESP32-Cheap-Yellow-Display-Documentation-DE)
|
||||
|
||||
If you would like to contribure a translation, please name the repo with the language name or code in the repo name and you can link it here.
|
||||
|
||||
## Help Support what I do!
|
||||
|
||||
[If you enjoy my work, please consider becoming a Github sponsor!](https://github.com/sponsors/witnessmenow/)
|
||||
39
firmware/cyd_esp32_2432s028/docs/SETUP.md
Normal file
39
firmware/cyd_esp32_2432s028/docs/SETUP.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Setup and Configuration options
|
||||
|
||||
This page will cover the basics of setting up the CYD
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
There really is nothing to setup here, just connect the CYD to a computer using a micro USB cable (it even comes with one)
|
||||
|
||||
## Software Setup
|
||||
|
||||
The driver needs to be setup for uploading to the CYD, including webflashing projects.
|
||||
|
||||
### Driver
|
||||
|
||||
The CYD uses the CH340 USB to UART chip. If you do not have a driver already installed for this chip you may need to install one. Check out [Sparkfun's guide for installation instruction](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all)
|
||||
|
||||
## Coding Setup
|
||||
|
||||
Follow these instructions if you want to write new code for the CYD
|
||||
|
||||
### Board definition
|
||||
|
||||
You will need to have the ESP32 setup for your Arduino IDE, [instructions can be found here](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html).
|
||||
|
||||
You can then select basically any ESP32 board in the boards menu. (I usually use "ESP32 Dev Module", but it doesn't really matter)
|
||||
|
||||
If you see errors uploading a sketch, try setting board upload speed to `115200`
|
||||
|
||||
### Library Configuration
|
||||
|
||||
The CYD can work with a selection of different libraries, but the main one this repo will focus on is [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) as it is a fairly popular library for working with these types of displays and there are lots of examples.
|
||||
|
||||
This can be installed from the library manager by searching for "TFT_eSPI".
|
||||
|
||||
> Note: After install of the library, copy the file [User_Setup.h](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/User_Setup.h) to the `libraries\TFT_eSPI` Arduino folder. This sets up the library for use with this display.
|
||||
|
||||
### Examples
|
||||
|
||||
I have provided examples for you to try out to get some ideas or inspiration. [Check them out here.](/Examples/)
|
||||
45
firmware/cyd_esp32_2432s028/docs/TROUBLESHOOTING.md
Normal file
45
firmware/cyd_esp32_2432s028/docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# First, Make sure it's a CYD!
|
||||
|
||||
If you are having any issues, this is the first thing I would check!
|
||||
|
||||
The examples and information contained on this repo are for the **ESP32-2432S028** display only. The model number is written on the back of the display in gold writting, beside the speaker connector.
|
||||
|
||||
# Display is not turning on
|
||||
|
||||
If you are having issues getting the display working, the first thing I would try is [webflashing an existing project](/PROJECTS.md#projects-1). These will be known working code, and if it works correctly, it points to a software issue, not a hardware one.
|
||||
|
||||
## If the webflash project displays something on the screen
|
||||
|
||||
- Make sure you have put the [User_Setup.h](DisplayConfig/User_Setup.h) file in the correct location [as described here](/SETUP.md#library-configuration)
|
||||
- Pin 21 is the backlight pin, make sure you are not using it for something else in your sketch.
|
||||
|
||||
## The webflash project doesn't display on screen
|
||||
|
||||
- Make sure you are not connecting Pin 21 to anything. It is broken out on the connector labeled `P3`
|
||||
- Try a different USB supply and or cable
|
||||
- If nothing else worked, your CYD could be faulty. Contact the seller.
|
||||
|
||||
# Display, Touch and SD card are not working at the same time
|
||||
|
||||
The ESP32 offers two usable hardware SPI buses, but on the CYD each of display, touch and SD card use a different bus. To use all three devices at the same time, for one of them the SPI has to be "simulated" in software. Usually this is done for the touch device, since it doesn't require a high bandwidth. Therefor use a software SPI implementation like [XPT2046_Bitbang_Slim](https://github.com/TheNitek/XPT2046_Bitbang_Arduino_Library) and follow the [button example](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/tree/main/Examples/Basics/8-Buttons)
|
||||
|
||||
# Display is flickering
|
||||
|
||||
- Try a different USB supply and or cable
|
||||
- Go through the [Display is not turning on](#display-is-not-turning-on) steps
|
||||
- If nothing else worked, your CYD could be faulty. Contact the seller.
|
||||
|
||||
# Cannot upload
|
||||
- On Ubuntu and flavors disable or uninstall service `brltty` and make sure user is in group `dialout`
|
||||
|
||||
# Automatic flash with esptool failed: Wrong boot mode detected (0x13)
|
||||
|
||||
This is the well-known problem of flashing ESP32 through USB-UART converter, when DTR and RTS signals are used to switch the chip to the bootloader mode (with additional 2xNPN transistor digital protection logic). On some PC, OS, driver version it works, on another it doesn't:
|
||||
|
||||
```
|
||||
A fatal error occurred: Failed to connect to ESP32: Wrong boot mode detected (0x13)! The chip needs to be in download mode. For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
|
||||
```
|
||||
|
||||
The solution is to replace a capacitor between EN (RST) and GND from 0.1uF, installed on CYD, to something in range 1uF and 10uF.
|
||||
|
||||
**NOTE:** In schematic, this is C4, but at least on Type-C version of CYD it is C5 actually.
|
||||
43
firmware/cyd_esp32_2432s028/docs/cyd.md
Normal file
43
firmware/cyd_esp32_2432s028/docs/cyd.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## What is a Cheap Yellow Display (CYD)?
|
||||
|
||||
A CYD is a ESP32-2432S028, an ESP32 development board with a 2.8" display with a resistive touch screen,
|
||||
|
||||
There are other boards with different sizes displays that look similar but **are not** a CYD. This isn't to try exclude anyone, but there so many different displays and types that it would be incredibly difficult and very confusing to support all of them.
|
||||
|
||||
You can verify you have the correct board by checking the number on the back of the display.
|
||||
|
||||

|
||||
|
||||
## My CYD has two USB ports
|
||||
|
||||
The original CYD only has a micro USB port, but there is a device that is also labelled a _ESP32-2432S028_ that has two USB ports, one micro USB and one USB-C.
|
||||
|
||||
Having an additional USB port would be a minor problem if that was the only difference, but unfortunately the display also works differently, the colours are inverted on the display.
|
||||
|
||||
It can be fixed in a couple of ways:
|
||||
|
||||
- Use platformio - The examples on the Github have all been updated so they can be used with platformio, and you can simply select CYD or CYD2USB and it will just work
|
||||
- Use the [CYD2USB specific User_setup.h](/DisplayConfig/CYD2USB/) that is on the repo, you can now use all the examples like normal
|
||||
- Invert the display at the code level using the `tft.invertDisplay(1);` method
|
||||
|
||||
### The USB-C port doesn't work
|
||||
|
||||
The USB-C port has a flaw in it, it doesn’t have the resistors on the CC lines. This means it will not work with USB-C to USB-C cables. If your computer only has USB-C ports, you can use it through a USB-C to USB-A adaptor.
|
||||
|
||||
### The Display doesn't look as good
|
||||
|
||||
There seems to be a gamma issue with the CYD2USB (I don't even know what gamma is)
|
||||
|
||||
Adding this to the code seems to help
|
||||
|
||||
```
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(2);
|
||||
delay(120);
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(1);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
38
firmware/cyd_esp32_2432s028/main/CMakeLists.txt
Normal file
38
firmware/cyd_esp32_2432s028/main/CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"ili9341.c"
|
||||
"touch.c"
|
||||
"display.c"
|
||||
"uart_transport.c"
|
||||
"buttons.c"
|
||||
"ui.c"
|
||||
"bech32.c"
|
||||
"mnemonic.c"
|
||||
"key_derivation.c"
|
||||
"pq_crypto_firmware.c"
|
||||
"secure_mem.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip004.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip044.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nostr_common.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/utils.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_aes.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_chacha20.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_secp256k1.c"
|
||||
"../../../resources/nostr_core_lib/platform/esp32/nostr_platform_esp32.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"../../../resources/nostr_core_lib/nostr_core"
|
||||
REQUIRES
|
||||
lvgl
|
||||
esp_timer
|
||||
esp_driver_gpio
|
||||
esp_driver_ledc
|
||||
esp_driver_spi
|
||||
esp_driver_uart
|
||||
mbedtls
|
||||
secp256k1
|
||||
pqclean
|
||||
json)
|
||||
|
||||
target_compile_definitions(${COMPONENT_LIB} PUBLIC LV_CONF_INCLUDE_SIMPLE=1)
|
||||
139
firmware/cyd_esp32_2432s028/main/bech32.c
Normal file
139
firmware/cyd_esp32_2432s028/main/bech32.c
Normal file
@@ -0,0 +1,139 @@
|
||||
#include "bech32.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char BECH32_CHARSET[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
static uint32_t bech32_polymod_step(uint32_t pre)
|
||||
{
|
||||
const uint8_t b = (uint8_t)(pre >> 25);
|
||||
return ((pre & 0x1FFFFFFu) << 5) ^
|
||||
((-(int32_t)((b >> 0) & 1u)) & 0x3b6a57b2u) ^
|
||||
((-(int32_t)((b >> 1) & 1u)) & 0x26508e6du) ^
|
||||
((-(int32_t)((b >> 2) & 1u)) & 0x1ea119fau) ^
|
||||
((-(int32_t)((b >> 3) & 1u)) & 0x3d4233ddu) ^
|
||||
((-(int32_t)((b >> 4) & 1u)) & 0x2a1462b3u);
|
||||
}
|
||||
|
||||
static int convert_bits(uint8_t *out,
|
||||
size_t *out_len,
|
||||
int out_bits,
|
||||
const uint8_t *in,
|
||||
size_t in_len,
|
||||
int in_bits,
|
||||
int pad)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
int bits = 0;
|
||||
const uint32_t max_v = (((uint32_t)1) << out_bits) - 1;
|
||||
|
||||
*out_len = 0;
|
||||
|
||||
while (in_len-- > 0) {
|
||||
value = (value << in_bits) | *(in++);
|
||||
bits += in_bits;
|
||||
|
||||
while (bits >= out_bits) {
|
||||
bits -= out_bits;
|
||||
out[(*out_len)++] = (uint8_t)((value >> bits) & max_v);
|
||||
}
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
out[(*out_len)++] = (uint8_t)((value << (out_bits - bits)) & max_v);
|
||||
}
|
||||
} else if (((value << (out_bits - bits)) & max_v) != 0u || bits >= in_bits) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_encode(char *output, size_t output_len, const char *hrp, const uint8_t *data, size_t data_len)
|
||||
{
|
||||
uint32_t chk = 1;
|
||||
size_t hrp_len = strlen(hrp);
|
||||
size_t pos = 0;
|
||||
|
||||
if (output == NULL || hrp == NULL || data == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < hrp_len; ++i) {
|
||||
const unsigned char ch = (unsigned char)hrp[i];
|
||||
if (ch < 33 || ch > 126 || (ch >= 'A' && ch <= 'Z')) {
|
||||
return 0;
|
||||
}
|
||||
chk = bech32_polymod_step(chk) ^ (ch >> 5);
|
||||
}
|
||||
|
||||
chk = bech32_polymod_step(chk);
|
||||
|
||||
for (size_t i = 0; i < hrp_len; ++i) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = hrp[i];
|
||||
}
|
||||
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = '1';
|
||||
|
||||
for (size_t i = 0; i < data_len; ++i) {
|
||||
if ((data[i] >> 5) != 0u) {
|
||||
return 0;
|
||||
}
|
||||
chk = bech32_polymod_step(chk) ^ data[i];
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = BECH32_CHARSET[data[i]];
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
chk = bech32_polymod_step(chk);
|
||||
}
|
||||
|
||||
chk ^= 1;
|
||||
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = BECH32_CHARSET[(chk >> ((5 - i) * 5)) & 0x1f];
|
||||
}
|
||||
|
||||
if (pos >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
output[pos] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
int npub_encode(const uint8_t pubkey32[32], char *out, size_t out_len)
|
||||
{
|
||||
uint8_t conv[64] = {0};
|
||||
size_t conv_len = 0;
|
||||
|
||||
if (pubkey32 == NULL || out == NULL || out_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!convert_bits(conv, &conv_len, 5, pubkey32, 32, 8, 1)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!bech32_encode(out, out_len, "npub", conv, conv_len)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(conv, 0, sizeof(conv));
|
||||
return 0;
|
||||
}
|
||||
6
firmware/cyd_esp32_2432s028/main/bech32.h
Normal file
6
firmware/cyd_esp32_2432s028/main/bech32.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int npub_encode(const uint8_t pubkey32[32], char *out, size_t out_len);
|
||||
30
firmware/cyd_esp32_2432s028/main/buttons.c
Normal file
30
firmware/cyd_esp32_2432s028/main/buttons.c
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "buttons.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
void buttons_init(void)
|
||||
{
|
||||
}
|
||||
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms)
|
||||
{
|
||||
if (timeout_ms != 0xFFFFFFFFu) {
|
||||
vTaskDelay(pdMS_TO_TICKS(timeout_ms));
|
||||
}
|
||||
|
||||
btn_event_t evt = {
|
||||
.id = BTN_NONE,
|
||||
.kind = BTN_EVT_NONE,
|
||||
};
|
||||
return evt;
|
||||
}
|
||||
|
||||
btn_event_t buttons_poll(void)
|
||||
{
|
||||
btn_event_t evt = {
|
||||
.id = BTN_NONE,
|
||||
.kind = BTN_EVT_NONE,
|
||||
};
|
||||
return evt;
|
||||
}
|
||||
33
firmware/cyd_esp32_2432s028/main/buttons.h
Normal file
33
firmware/cyd_esp32_2432s028/main/buttons.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
BTN_NONE = 0,
|
||||
BTN_D0,
|
||||
BTN_D1,
|
||||
BTN_D2,
|
||||
} btn_id_t;
|
||||
|
||||
typedef enum {
|
||||
BTN_EVT_NONE = 0,
|
||||
BTN_EVT_PRESS,
|
||||
BTN_EVT_LONG_PRESS,
|
||||
} btn_event_kind_t;
|
||||
|
||||
typedef struct {
|
||||
btn_id_t id;
|
||||
btn_event_kind_t kind;
|
||||
} btn_event_t;
|
||||
|
||||
void buttons_init(void);
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms);
|
||||
btn_event_t buttons_poll(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
160
firmware/cyd_esp32_2432s028/main/display.c
Normal file
160
firmware/cyd_esp32_2432s028/main/display.c
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ili9341.h"
|
||||
|
||||
#define CHAR_W_SMALL 6
|
||||
#define CHAR_H_SMALL 8
|
||||
#define CHAR_W_LARGE 12
|
||||
#define CHAR_H_LARGE 16
|
||||
|
||||
static bool s_inited = false;
|
||||
|
||||
static int text_width_px(const char *text, int char_w)
|
||||
{
|
||||
if (text == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return (int)strlen(text) * char_w;
|
||||
}
|
||||
|
||||
esp_err_t display_init(void)
|
||||
{
|
||||
if (s_inited) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(ili9341_init());
|
||||
s_inited = true;
|
||||
return display_clear(RGB565_BLACK);
|
||||
}
|
||||
|
||||
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
w += x;
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
h += y;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
if (x + w > DISPLAY_WIDTH) {
|
||||
w = DISPLAY_WIDTH - x;
|
||||
}
|
||||
if (y + h > DISPLAY_HEIGHT) {
|
||||
h = DISPLAY_HEIGHT - y;
|
||||
}
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
for (int py = y; py < y + h; ++py) {
|
||||
for (int px = x; px < x + w; ++px) {
|
||||
ESP_ERROR_CHECK(ili9341_draw_pixel((uint16_t)px, (uint16_t)py, color));
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_clear(uint16_t color)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ili9341_fill_screen(color);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ili9341_draw_text(x, y, text, fg, bg);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ili9341_draw_text(x, y, text, fg, bg);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int w = text_width_px(text, CHAR_W_LARGE);
|
||||
int x = (DISPLAY_WIDTH - w) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int w = text_width_px(text, CHAR_W_SMALL);
|
||||
int x = (DISPLAY_WIDTH - w) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text_small(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg)
|
||||
{
|
||||
return display_draw_text_small(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
const int slot_w = DISPLAY_WIDTH / 3;
|
||||
const int slot_h = 22;
|
||||
const int x = btn_id * slot_w;
|
||||
const int y = DISPLAY_HEIGHT - slot_h;
|
||||
|
||||
uint16_t fill = pressed ? RGB565_RED : RGB565_DIM_GRAY;
|
||||
uint16_t text = RGB565_WHITE;
|
||||
|
||||
ESP_ERROR_CHECK(display_fill_rect(x, y, slot_w - 1, slot_h - 1, fill));
|
||||
|
||||
char label[8];
|
||||
snprintf(label, sizeof(label), "D%d", btn_id);
|
||||
ESP_ERROR_CHECK(display_draw_text_small(x + (slot_w / 2) - 6, y + 7, label, text, fill));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
37
firmware/cyd_esp32_2432s028/main/display.h
Normal file
37
firmware/cyd_esp32_2432s028/main/display.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DISPLAY_WIDTH 320
|
||||
#define DISPLAY_HEIGHT 240
|
||||
|
||||
#define RGB565_BLACK 0x0000
|
||||
#define RGB565_WHITE 0xFFFF
|
||||
#define RGB565_RED 0xF800
|
||||
#define RGB565_GREEN 0x07E0
|
||||
#define RGB565_BLUE 0x001F
|
||||
#define RGB565_YELLOW 0xFFE0
|
||||
#define RGB565_CYAN 0x07FF
|
||||
#define RGB565_MAGENTA 0xF81F
|
||||
#define RGB565_DIM_GRAY 0x4208
|
||||
|
||||
esp_err_t display_init(void);
|
||||
esp_err_t display_clear(uint16_t color);
|
||||
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color);
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg);
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
124
firmware/cyd_esp32_2432s028/main/font5x7.h
Normal file
124
firmware/cyd_esp32_2432s028/main/font5x7.h
Normal file
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 5x7 font, ASCII 0x20..0x7F (printable chars + DEL placeholder)
|
||||
static const uint8_t font5x7[96][5] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00}, // 0x20 ' '
|
||||
{0x00, 0x00, 0x5F, 0x00, 0x00}, // 0x21 '!'
|
||||
{0x00, 0x07, 0x00, 0x07, 0x00}, // 0x22 '"'
|
||||
{0x14, 0x7F, 0x14, 0x7F, 0x14}, // 0x23 '#'
|
||||
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // 0x24 '$'
|
||||
{0x23, 0x13, 0x08, 0x64, 0x62}, // 0x25 '%'
|
||||
{0x36, 0x49, 0x55, 0x22, 0x50}, // 0x26 '&'
|
||||
{0x00, 0x05, 0x03, 0x00, 0x00}, // 0x27 '\''
|
||||
{0x00, 0x1C, 0x22, 0x41, 0x00}, // 0x28 '('
|
||||
{0x00, 0x41, 0x22, 0x1C, 0x00}, // 0x29 ')'
|
||||
{0x14, 0x08, 0x3E, 0x08, 0x14}, // 0x2A '*'
|
||||
{0x08, 0x08, 0x3E, 0x08, 0x08}, // 0x2B '+'
|
||||
{0x00, 0x50, 0x30, 0x00, 0x00}, // 0x2C ','
|
||||
{0x08, 0x08, 0x08, 0x08, 0x08}, // 0x2D '-'
|
||||
{0x00, 0x60, 0x60, 0x00, 0x00}, // 0x2E '.'
|
||||
{0x20, 0x10, 0x08, 0x04, 0x02}, // 0x2F '/'
|
||||
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0x30 '0'
|
||||
{0x00, 0x42, 0x7F, 0x40, 0x00}, // 0x31 '1'
|
||||
{0x42, 0x61, 0x51, 0x49, 0x46}, // 0x32 '2'
|
||||
{0x21, 0x41, 0x45, 0x4B, 0x31}, // 0x33 '3'
|
||||
{0x18, 0x14, 0x12, 0x7F, 0x10}, // 0x34 '4'
|
||||
{0x27, 0x45, 0x45, 0x45, 0x39}, // 0x35 '5'
|
||||
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // 0x36 '6'
|
||||
{0x01, 0x71, 0x09, 0x05, 0x03}, // 0x37 '7'
|
||||
{0x36, 0x49, 0x49, 0x49, 0x36}, // 0x38 '8'
|
||||
{0x06, 0x49, 0x49, 0x29, 0x1E}, // 0x39 '9'
|
||||
{0x00, 0x36, 0x36, 0x00, 0x00}, // 0x3A ':'
|
||||
{0x00, 0x56, 0x36, 0x00, 0x00}, // 0x3B ';'
|
||||
{0x08, 0x14, 0x22, 0x41, 0x00}, // 0x3C '<'
|
||||
{0x14, 0x14, 0x14, 0x14, 0x14}, // 0x3D '='
|
||||
{0x00, 0x41, 0x22, 0x14, 0x08}, // 0x3E '>'
|
||||
{0x02, 0x01, 0x51, 0x09, 0x06}, // 0x3F '?'
|
||||
{0x32, 0x49, 0x79, 0x41, 0x3E}, // 0x40 '@'
|
||||
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // 0x41 'A'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 0x42 'B'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 0x43 'C'
|
||||
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 0x44 'D'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 0x45 'E'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 0x46 'F'
|
||||
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 0x47 'G'
|
||||
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 0x48 'H'
|
||||
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 0x49 'I'
|
||||
{0x20, 0x40, 0x41, 0x3F, 0x01}, // 0x4A 'J'
|
||||
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 0x4B 'K'
|
||||
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 0x4C 'L'
|
||||
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 0x4D 'M'
|
||||
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 0x4E 'N'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 0x4F 'O'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 0x50 'P'
|
||||
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 0x51 'Q'
|
||||
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 0x52 'R'
|
||||
{0x46, 0x49, 0x49, 0x49, 0x31}, // 0x53 'S'
|
||||
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 0x54 'T'
|
||||
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 0x55 'U'
|
||||
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 0x56 'V'
|
||||
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // 0x57 'W'
|
||||
{0x63, 0x14, 0x08, 0x14, 0x63}, // 0x58 'X'
|
||||
{0x07, 0x08, 0x70, 0x08, 0x07}, // 0x59 'Y'
|
||||
{0x61, 0x51, 0x49, 0x45, 0x43}, // 0x5A 'Z'
|
||||
{0x00, 0x7F, 0x41, 0x41, 0x00}, // 0x5B '['
|
||||
{0x02, 0x04, 0x08, 0x10, 0x20}, // 0x5C '\\'
|
||||
{0x00, 0x41, 0x41, 0x7F, 0x00}, // 0x5D ']'
|
||||
{0x04, 0x02, 0x01, 0x02, 0x04}, // 0x5E '^'
|
||||
{0x40, 0x40, 0x40, 0x40, 0x40}, // 0x5F '_'
|
||||
{0x00, 0x01, 0x02, 0x04, 0x00}, // 0x60 '`'
|
||||
{0x20, 0x54, 0x54, 0x54, 0x78}, // 0x61 'a'
|
||||
{0x7F, 0x48, 0x44, 0x44, 0x38}, // 0x62 'b'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x20}, // 0x63 'c'
|
||||
{0x38, 0x44, 0x44, 0x48, 0x7F}, // 0x64 'd'
|
||||
{0x38, 0x54, 0x54, 0x54, 0x18}, // 0x65 'e'
|
||||
{0x08, 0x7E, 0x09, 0x01, 0x02}, // 0x66 'f'
|
||||
{0x0C, 0x52, 0x52, 0x52, 0x3E}, // 0x67 'g'
|
||||
{0x7F, 0x08, 0x04, 0x04, 0x78}, // 0x68 'h'
|
||||
{0x00, 0x44, 0x7D, 0x40, 0x00}, // 0x69 'i'
|
||||
{0x20, 0x40, 0x44, 0x3D, 0x00}, // 0x6A 'j'
|
||||
{0x7F, 0x10, 0x28, 0x44, 0x00}, // 0x6B 'k'
|
||||
{0x00, 0x41, 0x7F, 0x40, 0x00}, // 0x6C 'l'
|
||||
{0x7C, 0x04, 0x18, 0x04, 0x78}, // 0x6D 'm'
|
||||
{0x7C, 0x08, 0x04, 0x04, 0x78}, // 0x6E 'n'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x38}, // 0x6F 'o'
|
||||
{0x7C, 0x14, 0x14, 0x14, 0x08}, // 0x70 'p'
|
||||
{0x08, 0x14, 0x14, 0x18, 0x7C}, // 0x71 'q'
|
||||
{0x7C, 0x08, 0x04, 0x04, 0x08}, // 0x72 'r'
|
||||
{0x48, 0x54, 0x54, 0x54, 0x20}, // 0x73 's'
|
||||
{0x04, 0x3F, 0x44, 0x40, 0x20}, // 0x74 't'
|
||||
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // 0x75 'u'
|
||||
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // 0x76 'v'
|
||||
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // 0x77 'w'
|
||||
{0x44, 0x28, 0x10, 0x28, 0x44}, // 0x78 'x'
|
||||
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // 0x79 'y'
|
||||
{0x44, 0x64, 0x54, 0x4C, 0x44}, // 0x7A 'z'
|
||||
{0x00, 0x08, 0x36, 0x41, 0x00}, // 0x7B '{'
|
||||
{0x00, 0x00, 0x7F, 0x00, 0x00}, // 0x7C '|'
|
||||
{0x00, 0x41, 0x36, 0x08, 0x00}, // 0x7D '}'
|
||||
{0x08, 0x04, 0x08, 0x10, 0x08}, // 0x7E '~'
|
||||
{0x7F, 0x41, 0x41, 0x41, 0x7F}, // 0x7F DEL placeholder
|
||||
};
|
||||
|
||||
static inline bool font5x7_get(char c, uint8_t out[5])
|
||||
{
|
||||
if (out == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t uc = (uint8_t)c;
|
||||
if (uc < 0x20 || uc > 0x7F) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *glyph = font5x7[uc - 0x20];
|
||||
out[0] = glyph[0];
|
||||
out[1] = glyph[1];
|
||||
out[2] = glyph[2];
|
||||
out[3] = glyph[3];
|
||||
out[4] = glyph[4];
|
||||
return true;
|
||||
}
|
||||
4
firmware/cyd_esp32_2432s028/main/idf_component.yml
Normal file
4
firmware/cyd_esp32_2432s028/main/idf_component.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies:
|
||||
idf:
|
||||
version: ">=5.0.0"
|
||||
lvgl/lvgl: "^8.3.11"
|
||||
278
firmware/cyd_esp32_2432s028/main/ili9341.c
Normal file
278
firmware/cyd_esp32_2432s028/main/ili9341.c
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "ili9341.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "font5x7.h"
|
||||
|
||||
#define TFT_PIN_MISO GPIO_NUM_12
|
||||
#define TFT_PIN_MOSI GPIO_NUM_13
|
||||
#define TFT_PIN_SCLK GPIO_NUM_14
|
||||
#define TFT_PIN_CS GPIO_NUM_15
|
||||
#define TFT_PIN_DC GPIO_NUM_2
|
||||
#define TFT_PIN_BL GPIO_NUM_21
|
||||
|
||||
// Rotated additional 90° CCW from previous baseline; logical drawing space is 320x240.
|
||||
#define TFT_WIDTH 320
|
||||
#define TFT_HEIGHT 240
|
||||
|
||||
#define ILI9341_SWRESET 0x01
|
||||
#define ILI9341_SLPOUT 0x11
|
||||
#define ILI9341_GAMMASET 0x26
|
||||
#define ILI9341_DISPON 0x29
|
||||
#define ILI9341_CASET 0x2A
|
||||
#define ILI9341_PASET 0x2B
|
||||
#define ILI9341_RAMWR 0x2C
|
||||
#define ILI9341_MADCTL 0x36
|
||||
#define ILI9341_COLMOD 0x3A
|
||||
#define ILI9341_INVOFF 0x20
|
||||
|
||||
static const char *TAG = "ili9341";
|
||||
|
||||
static spi_device_handle_t s_spi;
|
||||
static bool s_initialized;
|
||||
|
||||
static esp_err_t ili9341_write_cmd(uint8_t cmd)
|
||||
{
|
||||
gpio_set_level(TFT_PIN_DC, 0);
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 8,
|
||||
.tx_buffer = &cmd,
|
||||
};
|
||||
|
||||
return spi_device_polling_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static esp_err_t ili9341_write_data(const void *data, size_t len)
|
||||
{
|
||||
if (len == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
gpio_set_level(TFT_PIN_DC, 1);
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = len * 8,
|
||||
.tx_buffer = data,
|
||||
};
|
||||
|
||||
return spi_device_polling_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static esp_err_t ili9341_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
|
||||
{
|
||||
uint8_t data[4];
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_CASET), TAG, "CASET failed");
|
||||
data[0] = (uint8_t)(x0 >> 8);
|
||||
data[1] = (uint8_t)(x0 & 0xFF);
|
||||
data[2] = (uint8_t)(x1 >> 8);
|
||||
data[3] = (uint8_t)(x1 & 0xFF);
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(data, sizeof(data)), TAG, "CASET data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_PASET), TAG, "PASET failed");
|
||||
data[0] = (uint8_t)(y0 >> 8);
|
||||
data[1] = (uint8_t)(y0 & 0xFF);
|
||||
data[2] = (uint8_t)(y1 >> 8);
|
||||
data[3] = (uint8_t)(y1 & 0xFF);
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(data, sizeof(data)), TAG, "PASET data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_RAMWR), TAG, "RAMWR failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ili9341_draw_pixel(uint16_t x, uint16_t y, uint16_t color)
|
||||
{
|
||||
if (x >= TFT_WIDTH || y >= TFT_HEIGHT) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t pix[2] = {
|
||||
(uint8_t)(color >> 8),
|
||||
(uint8_t)(color & 0xFF),
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_set_window(x, y, x, y), TAG, "set_window failed");
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(pix, sizeof(pix)), TAG, "pixel write failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ili9341_blit_rgb565(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *rgb565_be)
|
||||
{
|
||||
if (w == 0 || h == 0 || rgb565_be == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint16_t x1 = (uint16_t)(x + w - 1);
|
||||
uint16_t y1 = (uint16_t)(y + h - 1);
|
||||
|
||||
if (x >= TFT_WIDTH || y >= TFT_HEIGHT || x1 >= TFT_WIDTH || y1 >= TFT_HEIGHT) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_set_window(x, y, x1, y1), TAG, "set_window failed");
|
||||
return ili9341_write_data(rgb565_be, (size_t)w * h * 2);
|
||||
}
|
||||
|
||||
esp_err_t ili9341_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
gpio_config_t dc_cfg = {
|
||||
.pin_bit_mask = 1ULL << TFT_PIN_DC,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(gpio_config(&dc_cfg), TAG, "DC gpio config failed");
|
||||
|
||||
gpio_config_t bl_cfg = {
|
||||
.pin_bit_mask = 1ULL << TFT_PIN_BL,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(gpio_config(&bl_cfg), TAG, "BL gpio config failed");
|
||||
gpio_set_level(TFT_PIN_BL, 0);
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = TFT_PIN_MOSI,
|
||||
.miso_io_num = TFT_PIN_MISO,
|
||||
.sclk_io_num = TFT_PIN_SCLK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = TFT_WIDTH * 20 * 2,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(spi_bus_initialize(HSPI_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi_bus_initialize failed");
|
||||
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = 40 * 1000 * 1000,
|
||||
.mode = 0,
|
||||
.spics_io_num = TFT_PIN_CS,
|
||||
.queue_size = 4,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(spi_bus_add_device(HSPI_HOST, &devcfg, &s_spi), TAG, "spi_bus_add_device failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_SWRESET), TAG, "SWRESET failed");
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_SLPOUT), TAG, "SLPOUT failed");
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
|
||||
const uint8_t colmod = 0x55;
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_COLMOD), TAG, "COLMOD cmd failed");
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(&colmod, 1), TAG, "COLMOD data failed");
|
||||
|
||||
const uint8_t madctl = 0x28; // MV | BGR (180° from previous 320x240 orientation)
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_MADCTL), TAG, "MADCTL cmd failed");
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(&madctl, 1), TAG, "MADCTL data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_INVOFF), TAG, "INVOFF failed");
|
||||
|
||||
const uint8_t gamma_2 = 0x02;
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_GAMMASET), TAG, "GAMMASET #1 cmd failed");
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(&gamma_2, 1), TAG, "GAMMASET #1 data failed");
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
|
||||
const uint8_t gamma_1 = 0x01;
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_GAMMASET), TAG, "GAMMASET #2 cmd failed");
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_data(&gamma_1, 1), TAG, "GAMMASET #2 data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(ili9341_write_cmd(ILI9341_DISPON), TAG, "DISPON failed");
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
|
||||
gpio_set_level(TFT_PIN_BL, 1);
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "ILI9341 initialized (320x240, HSPI @ 40MHz)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ili9341_fill_screen(uint16_t color)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int chunk_lines = 20;
|
||||
const size_t chunk_bytes = TFT_WIDTH * chunk_lines * 2;
|
||||
uint8_t *buf = (uint8_t *)malloc(chunk_bytes);
|
||||
if (!buf) {
|
||||
ESP_LOGE(TAG, "malloc failed in fill_screen");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < TFT_WIDTH * chunk_lines; ++i) {
|
||||
buf[i * 2 + 0] = (uint8_t)(color >> 8);
|
||||
buf[i * 2 + 1] = (uint8_t)(color & 0xFF);
|
||||
}
|
||||
|
||||
for (int y = 0; y < TFT_HEIGHT; y += chunk_lines) {
|
||||
int y_end = y + chunk_lines - 1;
|
||||
if (y_end >= TFT_HEIGHT) {
|
||||
y_end = TFT_HEIGHT - 1;
|
||||
}
|
||||
|
||||
int lines = y_end - y + 1;
|
||||
size_t bytes = (size_t)TFT_WIDTH * lines * 2;
|
||||
|
||||
if (ili9341_set_window(0, (uint16_t)y, TFT_WIDTH - 1, (uint16_t)y_end) == ESP_OK) {
|
||||
(void)ili9341_write_data(buf, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void ili9341_draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
uint8_t cols[5];
|
||||
if (!font5x7_get(c, cols)) {
|
||||
c = ' ';
|
||||
(void)font5x7_get(c, cols);
|
||||
}
|
||||
|
||||
for (int cx = 0; cx < 5; ++cx) {
|
||||
uint8_t col = cols[cx];
|
||||
for (int cy = 0; cy < 8; ++cy) {
|
||||
bool on = (col >> cy) & 0x01;
|
||||
uint16_t color = on ? fg : bg;
|
||||
(void)ili9341_draw_pixel((uint16_t)(x + cx), (uint16_t)(y + cy), color);
|
||||
}
|
||||
}
|
||||
|
||||
for (int cy = 0; cy < 8; ++cy) {
|
||||
(void)ili9341_draw_pixel((uint16_t)(x + 5), (uint16_t)(y + cy), bg);
|
||||
}
|
||||
}
|
||||
|
||||
void ili9341_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_initialized || text == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cursor_x = x;
|
||||
while (*text) {
|
||||
if (cursor_x + 6 >= TFT_WIDTH) {
|
||||
break;
|
||||
}
|
||||
ili9341_draw_char(cursor_x, y, *text, fg, bg);
|
||||
cursor_x += 6;
|
||||
++text;
|
||||
}
|
||||
}
|
||||
17
firmware/cyd_esp32_2432s028/main/ili9341.h
Normal file
17
firmware/cyd_esp32_2432s028/main/ili9341.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define ILI9341_COLOR_BLACK 0x0000
|
||||
#define ILI9341_COLOR_WHITE 0xFFFF
|
||||
#define ILI9341_COLOR_RED 0xF800
|
||||
#define ILI9341_COLOR_GREEN 0x07E0
|
||||
#define ILI9341_COLOR_BLUE 0x001F
|
||||
|
||||
esp_err_t ili9341_init(void);
|
||||
void ili9341_fill_screen(uint16_t color);
|
||||
esp_err_t ili9341_draw_pixel(uint16_t x, uint16_t y, uint16_t color);
|
||||
esp_err_t ili9341_blit_rgb565(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *rgb565_be);
|
||||
void ili9341_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
611
firmware/cyd_esp32_2432s028/main/key_derivation.c
Normal file
611
firmware/cyd_esp32_2432s028/main/key_derivation.c
Normal file
@@ -0,0 +1,611 @@
|
||||
#include "key_derivation.h"
|
||||
#include "pq_crypto_firmware.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/ecp.h"
|
||||
#include "mbedtls/pk.h"
|
||||
#include "psa/crypto.h"
|
||||
|
||||
#include "secp256k1.h"
|
||||
#include "secp256k1_extrakeys.h"
|
||||
#include "secp256k1_schnorrsig.h"
|
||||
|
||||
static const char *KD_TAG = "key_derivation";
|
||||
|
||||
#define BIP32_HARDENED_FLAG 0x80000000u
|
||||
|
||||
typedef struct {
|
||||
uint8_t priv[32];
|
||||
uint8_t chain[32];
|
||||
uint8_t pub[33];
|
||||
} hd_key_t;
|
||||
|
||||
static int hmac_sha512(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *data,
|
||||
size_t data_len,
|
||||
uint8_t out[64])
|
||||
{
|
||||
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
if (md_info == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mbedtls_md_hmac(md_info, key, key_len, data, data_len, out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static secp256k1_context *create_context(void)
|
||||
{
|
||||
uint8_t rand32[32] = {0};
|
||||
secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
|
||||
|
||||
if (ctx == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_fill_random(rand32, sizeof(rand32));
|
||||
if (!secp256k1_context_randomize(ctx, rand32)) {
|
||||
memset(rand32, 0, sizeof(rand32));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(rand32, 0, sizeof(rand32));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static int compute_compressed_pubkey(const secp256k1_context *ctx, const uint8_t priv[32], uint8_t pub33[33])
|
||||
{
|
||||
secp256k1_pubkey pubkey;
|
||||
size_t out_len = 33;
|
||||
|
||||
if (!secp256k1_ec_pubkey_create(ctx, &pubkey, priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_ec_pubkey_serialize(ctx, pub33, &out_len, &pubkey, SECP256K1_EC_COMPRESSED)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (out_len != 33) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bip32_master_from_seed(const secp256k1_context *ctx, const uint8_t seed[64], hd_key_t *master)
|
||||
{
|
||||
static const uint8_t kBitcoinSeed[] = "Bitcoin seed";
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
if (hmac_sha512(kBitcoinSeed, sizeof(kBitcoinSeed) - 1, seed, 64, i64) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(master->priv, i64, 32);
|
||||
memcpy(master->chain, i64 + 32, 32);
|
||||
memset(i64, 0, sizeof(i64));
|
||||
|
||||
if (!secp256k1_ec_seckey_verify(ctx, master->priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return compute_compressed_pubkey(ctx, master->priv, master->pub);
|
||||
}
|
||||
|
||||
static int bip32_ckd_priv(const secp256k1_context *ctx,
|
||||
const hd_key_t *parent,
|
||||
uint32_t index,
|
||||
hd_key_t *child)
|
||||
{
|
||||
uint8_t data[37] = {0};
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
if (index & BIP32_HARDENED_FLAG) {
|
||||
data[0] = 0x00;
|
||||
memcpy(data + 1, parent->priv, 32);
|
||||
} else {
|
||||
memcpy(data, parent->pub, 33);
|
||||
}
|
||||
|
||||
data[33] = (uint8_t)((index >> 24) & 0xFF);
|
||||
data[34] = (uint8_t)((index >> 16) & 0xFF);
|
||||
data[35] = (uint8_t)((index >> 8) & 0xFF);
|
||||
data[36] = (uint8_t)(index & 0xFF);
|
||||
|
||||
if (hmac_sha512(parent->chain, 32, data, sizeof(data), i64) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(child->priv, parent->priv, 32);
|
||||
memcpy(child->chain, i64 + 32, 32);
|
||||
|
||||
if (!secp256k1_ec_seckey_tweak_add(ctx, child->priv, i64)) {
|
||||
memset(i64, 0, sizeof(i64));
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(i64, 0, sizeof(i64));
|
||||
|
||||
if (!secp256k1_ec_seckey_verify(ctx, child->priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return compute_compressed_pubkey(ctx, child->priv, child->pub);
|
||||
}
|
||||
|
||||
static int derive_nostr_key_internal(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
1237u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u,
|
||||
nostr_index,
|
||||
};
|
||||
|
||||
secp256k1_context *ctx = NULL;
|
||||
hd_key_t node;
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
|
||||
ctx = create_context();
|
||||
if (ctx == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (bip32_master_from_seed(ctx, seed, &node) != 0) {
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(path) / sizeof(path[0]); ++i) {
|
||||
hd_key_t next;
|
||||
memset(&next, 0, sizeof(next));
|
||||
|
||||
if (bip32_ckd_priv(ctx, &node, path[i], &next) != 0) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
node = next;
|
||||
}
|
||||
|
||||
memcpy(privkey, node.priv, 32);
|
||||
|
||||
{
|
||||
secp256k1_keypair kp;
|
||||
secp256k1_xonly_pubkey xonly;
|
||||
|
||||
if (!secp256k1_keypair_create(ctx, &kp, node.priv)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_keypair_xonly_pub(ctx, &xonly, NULL, &kp)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xonly)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, 0u, privkey, pubkey);
|
||||
}
|
||||
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, nostr_index, privkey, pubkey);
|
||||
}
|
||||
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64])
|
||||
{
|
||||
secp256k1_context *ctx = NULL;
|
||||
secp256k1_keypair kp;
|
||||
uint8_t aux[32] = {0};
|
||||
|
||||
if (privkey == NULL || msg32 == NULL || sig64 == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx = create_context();
|
||||
if (ctx == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_keypair_create(ctx, &kp, privkey)) {
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_fill_random(aux, sizeof(aux));
|
||||
if (!secp256k1_schnorrsig_sign32(ctx, sig64, msg32, &kp, aux)) {
|
||||
memset(aux, 0, sizeof(aux));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(aux, 0, sizeof(aux));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Phase 7: ed25519, x25519, and post-quantum key derivation
|
||||
* ==================================================================== */
|
||||
|
||||
/* SLIP-0010 all-hardened derivation for ed25519/x25519.
|
||||
*
|
||||
* SLIP-0010 uses HMAC-SHA512 with a "ed25519 seed" or curve-specific key
|
||||
* for the master key, and all derivation steps are hardened (the parent
|
||||
* private key is prepended to the index data).
|
||||
*
|
||||
* For ed25519/x25519, the derived 512-bit HMAC output is split:
|
||||
* - first 32 bytes = private key (the scalar)
|
||||
* - last 32 bytes = chain code
|
||||
*
|
||||
* The private key IS the ed25519/x25519 secret — no tweak-add is needed
|
||||
* (unlike secp256k1 BIP-32 where the child priv = parent_priv + HMAC).
|
||||
*/
|
||||
|
||||
/* SLIP-0010 master key from seed: HMAC-SHA512(key="ed25519 seed", data=seed) */
|
||||
static int slip10_master_from_seed(const uint8_t seed[64],
|
||||
uint8_t priv[32], uint8_t chain[32]) {
|
||||
static const uint8_t kEd25519Seed[] = "ed25519 seed";
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
if (hmac_sha512(kEd25519Seed, sizeof(kEd25519Seed) - 1,
|
||||
seed, 64, i64) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(priv, i64, 32);
|
||||
memcpy(chain, i64 + 32, 32);
|
||||
memset(i64, 0, sizeof(i64));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* SLIP-0010 hardened child derivation:
|
||||
* HMAC-SHA512(key=chain, data=0x00 || priv || index_be32) */
|
||||
static int slip10_ckd_priv(const uint8_t parent_priv[32],
|
||||
const uint8_t parent_chain[32],
|
||||
uint32_t index,
|
||||
uint8_t child_priv[32],
|
||||
uint8_t child_chain[32]) {
|
||||
uint8_t data[37];
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
/* Hardened derivation: 0x00 || priv || index (big-endian) */
|
||||
data[0] = 0x00;
|
||||
memcpy(data + 1, parent_priv, 32);
|
||||
data[33] = (uint8_t)((index >> 24) & 0xFF);
|
||||
data[34] = (uint8_t)((index >> 16) & 0xFF);
|
||||
data[35] = (uint8_t)((index >> 8) & 0xFF);
|
||||
data[36] = (uint8_t)(index & 0xFF);
|
||||
|
||||
if (hmac_sha512(parent_chain, 32, data, sizeof(data), i64) != 0) {
|
||||
memset(data, 0, sizeof(data));
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(child_priv, i64, 32);
|
||||
memcpy(child_chain, i64 + 32, 32);
|
||||
memset(data, 0, sizeof(data));
|
||||
memset(i64, 0, sizeof(i64));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Derive a 32-byte seed via SLIP-0010 all-hardened path.
|
||||
* path[] is an array of hardened indices (the caller sets the hardened flag).
|
||||
* Returns the final 32-byte private material in `out_seed`. */
|
||||
static int slip10_derive_seed(const uint8_t seed[64],
|
||||
const uint32_t *path, size_t path_len,
|
||||
uint8_t out_seed[32]) {
|
||||
uint8_t priv[32], chain[32], next_priv[32], next_chain[32];
|
||||
size_t i;
|
||||
|
||||
if (slip10_master_from_seed(seed, priv, chain) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < path_len; i++) {
|
||||
if (slip10_ckd_priv(priv, chain, path[i],
|
||||
next_priv, next_chain) != 0) {
|
||||
memset(priv, 0, sizeof(priv));
|
||||
memset(chain, 0, sizeof(chain));
|
||||
return -1;
|
||||
}
|
||||
memcpy(priv, next_priv, 32);
|
||||
memcpy(chain, next_chain, 32);
|
||||
}
|
||||
|
||||
memcpy(out_seed, priv, 32);
|
||||
memset(priv, 0, sizeof(priv));
|
||||
memset(chain, 0, sizeof(chain));
|
||||
memset(next_priv, 0, sizeof(next_priv));
|
||||
memset(next_chain, 0, sizeof(next_chain));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ed25519 --- */
|
||||
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]) {
|
||||
/* m/44'/102001'/<index>'/0'/0' — all hardened (SLIP-0010) */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102001u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t derived_seed[32];
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key. */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
|
||||
/* Derive the ed25519 public key via PSA crypto (IDF v5.x mbedtls has no
|
||||
* mbedtls_ed25519_make_public). Import the private key, export the pub. */
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_import failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_export_public failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ed25519 sign via PSA (PureEdDSA — signs the raw message, not pre-hashed). */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]) {
|
||||
return ed25519_sign_msg(privkey, msg32, 32, sig64);
|
||||
}
|
||||
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t sig_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 sign psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_sign_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig64, 64, &sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || sig_len != 64) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_sign_message failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, pubkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 verify psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_verify_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig, sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
return (status == PSA_SUCCESS) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --- x25519 --- */
|
||||
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]) {
|
||||
/* m/44'/102002'/<index>'/0'/0' — all hardened (SLIP-0010) */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102002u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t derived_seed[32];
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* The SLIP-0010 derived 32 bytes IS the x25519 private key.
|
||||
* PSA imports it and exports the public key (PSA handles clamping). */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_DERIVE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_ECDH);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_import failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_export_public failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ML-DSA-65 --- */
|
||||
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102003'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102003u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = fw_pq_ml_dsa_65_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- SLH-DSA-128s --- */
|
||||
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102004'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102004u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ESP_LOGW(KD_TAG, "SLH-DSA-128s keygen: this takes 5-30 seconds on ESP32");
|
||||
int ret = fw_pq_slh_dsa_128s_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- ML-KEM-768 --- */
|
||||
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102005'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102005u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = fw_pq_ml_kem_768_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
70
firmware/cyd_esp32_2432s028/main/key_derivation.h
Normal file
70
firmware/cyd_esp32_2432s028/main/key_derivation.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- secp256k1 (Nostr, existing) --- */
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
|
||||
|
||||
/* --- ed25519 (SSH signatures) --- */
|
||||
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
|
||||
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
|
||||
* privkey: 32-byte ed25519 private scalar
|
||||
* pubkey: 32-byte ed25519 public key
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
|
||||
/* Signs a 32-byte message digest with ed25519 (PureEdDSA, raw message).
|
||||
* sig: 64-byte ed25519 signature
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Signs a variable-length message with ed25519 (PureEdDSA). */
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Verifies an ed25519 signature over a variable-length message. */
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]);
|
||||
|
||||
/* --- x25519 (age encryption / key agreement) --- */
|
||||
/* Derives an x25519 keypair from the mnemonic seed using SLIP-0010
|
||||
* all-hardened derivation: m/44'/102002'/<n>'/0'/0'
|
||||
* privkey: 32-byte x25519 private scalar
|
||||
* pubkey: 32-byte x25519 public key
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
|
||||
/* --- ML-DSA-65 (post-quantum signatures, FIPS 204) --- */
|
||||
/* Derives an ML-DSA-65 keypair from the mnemonic seed.
|
||||
* Path: m/44'/102003'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_ML_DSA_65_PUBKEY_LEN (1952) bytes — caller must allocate
|
||||
* sk: FW_ML_DSA_65_PRIVKEY_LEN (4032) bytes — caller must allocate
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* --- SLH-DSA-128s (post-quantum hash-based signatures, FIPS 205) --- */
|
||||
/* Derives an SLH-DSA-128s keypair from the mnemonic seed.
|
||||
* Path: m/44'/102004'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_SLH_DSA_128S_PUBKEY_LEN (32) bytes
|
||||
* sk: FW_SLH_DSA_128S_PRIVKEY_LEN (64) bytes
|
||||
* WARNING: Takes 5-30 seconds on ESP32.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* --- ML-KEM-768 (post-quantum KEM, FIPS 203) --- */
|
||||
/* Derives an ML-KEM-768 keypair from the mnemonic seed.
|
||||
* Path: m/44'/102005'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_ML_KEM_768_PUBKEY_LEN (1184) bytes — caller must allocate
|
||||
* sk: FW_ML_KEM_768_PRIVKEY_LEN (2400) bytes — caller must allocate
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
31
firmware/cyd_esp32_2432s028/main/lv_conf.h
Normal file
31
firmware/cyd_esp32_2432s028/main/lv_conf.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#define LV_COLOR_DEPTH 16
|
||||
#define LV_COLOR_16_SWAP 0
|
||||
|
||||
#define LV_MEM_CUSTOM 0
|
||||
#define LV_MEM_SIZE (24U * 1024U)
|
||||
|
||||
#define LV_DISP_DEF_REFR_PERIOD 30
|
||||
|
||||
#define LV_USE_LOG 0
|
||||
#define LV_USE_ASSERT_NULL 1
|
||||
#define LV_USE_ASSERT_MEM 1
|
||||
#define LV_USE_ASSERT_OBJ 0
|
||||
|
||||
#define LV_USE_PERF_MONITOR 0
|
||||
|
||||
#define LV_FONT_MONTSERRAT_14 1
|
||||
#define LV_FONT_MONTSERRAT_16 1
|
||||
#define LV_FONT_DEFAULT &lv_font_montserrat_14
|
||||
|
||||
#define LV_USE_THEME_DEFAULT 1
|
||||
|
||||
#define LV_USE_LABEL 1
|
||||
#define LV_USE_BTN 1
|
||||
#define LV_USE_SLIDER 1
|
||||
|
||||
#define LV_USE_ANIMATION 1
|
||||
|
||||
#define LV_USE_DRAW_SW 1
|
||||
#define LV_USE_DRAW_SW_COMPLEX 0
|
||||
2573
firmware/cyd_esp32_2432s028/main/main.c
Normal file
2573
firmware/cyd_esp32_2432s028/main/main.c
Normal file
File diff suppressed because it is too large
Load Diff
390
firmware/cyd_esp32_2432s028/main/mnemonic.c
Normal file
390
firmware/cyd_esp32_2432s028/main/mnemonic.c
Normal file
@@ -0,0 +1,390 @@
|
||||
#include "mnemonic.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/pkcs5.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
|
||||
#include "mnemonic_wordlist.h"
|
||||
|
||||
#define MNEMONIC_12_WORDS 12
|
||||
#define ENTROPY_12_BYTES 16
|
||||
#define CHECKSUM_12_BITS 4
|
||||
#define TOTAL_12_BITS (ENTROPY_12_BYTES * 8 + CHECKSUM_12_BITS)
|
||||
|
||||
static bool s_letter_index_built = false;
|
||||
static size_t s_letter_start[26] = {0};
|
||||
static size_t s_letter_count[26] = {0};
|
||||
|
||||
static bool append_word(char *out, size_t out_len, size_t *cursor, const char *word)
|
||||
{
|
||||
const size_t word_len = strlen(word);
|
||||
|
||||
if (*cursor >= out_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*cursor != 0) {
|
||||
if (*cursor + 1 >= out_len) {
|
||||
return false;
|
||||
}
|
||||
out[*cursor] = ' ';
|
||||
(*cursor)++;
|
||||
}
|
||||
|
||||
if (*cursor + word_len >= out_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(out + *cursor, word, word_len);
|
||||
*cursor += word_len;
|
||||
out[*cursor] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t get_bit_from_entropy_checksum(const uint8_t entropy[ENTROPY_12_BYTES],
|
||||
const uint8_t checksum[32],
|
||||
size_t bit_index)
|
||||
{
|
||||
if (bit_index < ENTROPY_12_BYTES * 8) {
|
||||
const size_t byte_index = bit_index / 8;
|
||||
const size_t bit_in_byte = 7 - (bit_index % 8);
|
||||
return (uint8_t)((entropy[byte_index] >> bit_in_byte) & 0x01);
|
||||
}
|
||||
|
||||
const size_t checksum_bit_index = bit_index - (ENTROPY_12_BYTES * 8);
|
||||
const size_t bit_in_byte = 7 - checksum_bit_index;
|
||||
return (uint8_t)((checksum[0] >> bit_in_byte) & 0x01);
|
||||
}
|
||||
|
||||
static int bip39_word_index(const char *word)
|
||||
{
|
||||
if (word == NULL || word[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
if (strcmp(word, BIP39_WORDLIST[i]) == 0) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool next_token(const char **cursor, char *out_word, size_t out_len)
|
||||
{
|
||||
size_t n = 0;
|
||||
const char *p = *cursor;
|
||||
|
||||
while (*p != '\0' && isspace((unsigned char)*p)) {
|
||||
++p;
|
||||
}
|
||||
|
||||
if (*p == '\0') {
|
||||
*cursor = p;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*p != '\0' && !isspace((unsigned char)*p)) {
|
||||
if (n + 1 >= out_len) {
|
||||
return false;
|
||||
}
|
||||
out_word[n++] = *p++;
|
||||
}
|
||||
|
||||
out_word[n] = '\0';
|
||||
*cursor = p;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void build_letter_index(void)
|
||||
{
|
||||
if (s_letter_index_built) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 26; ++i) {
|
||||
s_letter_start[i] = 0;
|
||||
s_letter_count[i] = 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
char c = BIP39_WORDLIST[i][0];
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t li = (size_t)(c - 'a');
|
||||
if (s_letter_count[li] == 0) {
|
||||
s_letter_start[li] = i;
|
||||
}
|
||||
s_letter_count[li]++;
|
||||
}
|
||||
|
||||
s_letter_index_built = true;
|
||||
}
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len)
|
||||
{
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
uint8_t hash[32] = {0};
|
||||
size_t cursor = 0;
|
||||
|
||||
if (out == NULL || out_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out[0] = '\0';
|
||||
|
||||
esp_fill_random(entropy, sizeof(entropy));
|
||||
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
|
||||
|
||||
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
|
||||
const size_t start_bit = w * 11;
|
||||
uint16_t index = 0;
|
||||
|
||||
for (size_t b = 0; b < 11; ++b) {
|
||||
index = (uint16_t)((index << 1) |
|
||||
get_bit_from_entropy_checksum(entropy, hash, start_bit + b));
|
||||
}
|
||||
|
||||
if (index >= 2048) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!append_word(out, out_len, &cursor, BIP39_WORDLIST[index])) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (TOTAL_12_BITS != 132) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mnemonic_validate(const char *mnemonic)
|
||||
{
|
||||
uint16_t word_indices[MNEMONIC_12_WORDS] = {0};
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
uint8_t hash[32] = {0};
|
||||
const char *cursor = mnemonic;
|
||||
char token[16];
|
||||
size_t words = 0;
|
||||
size_t bit_pos = 0;
|
||||
|
||||
if (mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (next_token(&cursor, token, sizeof(token))) {
|
||||
int idx;
|
||||
|
||||
if (words >= MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = bip39_word_index(token);
|
||||
if (idx < 0) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
word_indices[words++] = (uint16_t)idx;
|
||||
}
|
||||
|
||||
if (words != MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
|
||||
for (int b = 10; b >= 0; --b) {
|
||||
uint8_t bit = (uint8_t)((word_indices[w] >> b) & 0x01);
|
||||
if (bit_pos < ENTROPY_12_BYTES * 8) {
|
||||
size_t byte_index = bit_pos / 8;
|
||||
size_t bit_in_byte = 7 - (bit_pos % 8);
|
||||
entropy[byte_index] |= (uint8_t)(bit << bit_in_byte);
|
||||
}
|
||||
bit_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
|
||||
|
||||
for (size_t i = 0; i < CHECKSUM_12_BITS; ++i) {
|
||||
size_t checksum_bit_pos = ENTROPY_12_BYTES * 8 + i;
|
||||
size_t word_idx = checksum_bit_pos / 11;
|
||||
size_t bit_in_word = 10 - (checksum_bit_pos % 11);
|
||||
uint8_t expected = (uint8_t)((hash[0] >> (7 - i)) & 0x01);
|
||||
uint8_t actual = (uint8_t)((word_indices[word_idx] >> bit_in_word) & 0x01);
|
||||
if (actual != expected) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start)
|
||||
{
|
||||
int idx;
|
||||
|
||||
if (out_start == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (letter >= 'A' && letter <= 'Z') {
|
||||
letter = (char)(letter - 'A' + 'a');
|
||||
}
|
||||
|
||||
if (letter < 'a' || letter > 'z') {
|
||||
*out_start = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
build_letter_index();
|
||||
|
||||
idx = letter - 'a';
|
||||
*out_start = s_letter_start[idx];
|
||||
return s_letter_count[idx];
|
||||
}
|
||||
|
||||
size_t mnemonic_words_with_prefix(const char *prefix, uint16_t *out_indices, size_t max_out)
|
||||
{
|
||||
size_t start = 0;
|
||||
size_t count = 0;
|
||||
size_t written = 0;
|
||||
|
||||
if (prefix == NULL || prefix[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
count = mnemonic_words_for_letter(prefix[0], &start);
|
||||
if (count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
size_t idx = start + i;
|
||||
const char *word = BIP39_WORDLIST[idx];
|
||||
if (strncmp(word, prefix, strlen(prefix)) == 0) {
|
||||
if (out_indices != NULL && written < max_out) {
|
||||
out_indices[written] = (uint16_t)idx;
|
||||
}
|
||||
written++;
|
||||
}
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
bool mnemonic_letter_extends_prefix(const char *prefix, size_t prefix_len, char next_letter)
|
||||
{
|
||||
char probe[16];
|
||||
size_t start = 0;
|
||||
size_t count = 0;
|
||||
|
||||
if (next_letter >= 'A' && next_letter <= 'Z') {
|
||||
next_letter = (char)(next_letter - 'A' + 'a');
|
||||
}
|
||||
|
||||
if (next_letter < 'a' || next_letter > 'z') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prefix == NULL || prefix_len >= sizeof(probe) - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(probe, prefix, prefix_len);
|
||||
probe[prefix_len] = next_letter;
|
||||
probe[prefix_len + 1] = '\0';
|
||||
|
||||
count = mnemonic_words_for_letter(probe[0], &start);
|
||||
if (count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const char *word = BIP39_WORDLIST[start + i];
|
||||
if (strncmp(word, probe, prefix_len + 1) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *mnemonic_word_at(size_t index)
|
||||
{
|
||||
if (index >= 2048) {
|
||||
return NULL;
|
||||
}
|
||||
return BIP39_WORDLIST[index];
|
||||
}
|
||||
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64])
|
||||
{
|
||||
const mbedtls_md_info_t *md_info;
|
||||
mbedtls_md_context_t ctx;
|
||||
uint8_t salt[8] = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'};
|
||||
|
||||
if (mnemonic == NULL || seed == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
if (md_info == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mbedtls_md_init(&ctx);
|
||||
if (mbedtls_md_setup(&ctx, md_info, 1) != 0) {
|
||||
mbedtls_md_free(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)ctx;
|
||||
if (mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA512,
|
||||
(const uint8_t *)mnemonic,
|
||||
strlen(mnemonic),
|
||||
salt,
|
||||
sizeof(salt),
|
||||
2048,
|
||||
64,
|
||||
seed) != 0) {
|
||||
mbedtls_md_free(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
mbedtls_md_free(&ctx);
|
||||
return 0;
|
||||
}
|
||||
13
firmware/cyd_esp32_2432s028/main/mnemonic.h
Normal file
13
firmware/cyd_esp32_2432s028/main/mnemonic.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len);
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]);
|
||||
int mnemonic_validate(const char *mnemonic);
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start);
|
||||
size_t mnemonic_words_with_prefix(const char *prefix, uint16_t *out_indices, size_t max_out);
|
||||
bool mnemonic_letter_extends_prefix(const char *prefix, size_t prefix_len, char next_letter);
|
||||
const char *mnemonic_word_at(size_t index);
|
||||
261
firmware/cyd_esp32_2432s028/main/mnemonic_wordlist.h
Normal file
261
firmware/cyd_esp32_2432s028/main/mnemonic_wordlist.h
Normal file
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
|
||||
/* BIP-39 English wordlist (2048 words) */
|
||||
static const char *BIP39_WORDLIST[2048] = {
|
||||
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
|
||||
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
|
||||
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
|
||||
"adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
|
||||
"advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
|
||||
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
|
||||
"alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone",
|
||||
"alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
|
||||
"amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry",
|
||||
"animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
|
||||
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april",
|
||||
"arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
|
||||
"army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact",
|
||||
"artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
|
||||
"asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
|
||||
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
|
||||
"avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
|
||||
"baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
|
||||
"bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base",
|
||||
"basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
|
||||
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt",
|
||||
"bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
|
||||
"bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black",
|
||||
"blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
|
||||
"blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
|
||||
"boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
|
||||
"borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain",
|
||||
"brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
|
||||
"bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother",
|
||||
"brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
|
||||
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus",
|
||||
"business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
|
||||
"cactus", "cage", "cake", "call", "calm", "camera", "camp", "can",
|
||||
"canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
|
||||
"capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
|
||||
"cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
|
||||
"catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling",
|
||||
"celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
|
||||
"champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap",
|
||||
"check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
|
||||
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar",
|
||||
"cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
|
||||
"claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff",
|
||||
"climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
|
||||
"clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
|
||||
"code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
|
||||
"come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm",
|
||||
"congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
|
||||
"copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch",
|
||||
"country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
|
||||
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream",
|
||||
"credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
|
||||
"cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch",
|
||||
"crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
|
||||
"current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
|
||||
"damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
|
||||
"day", "deal", "debate", "debris", "decade", "december", "decide", "decline",
|
||||
"decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
|
||||
"deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend",
|
||||
"deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
|
||||
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram",
|
||||
"dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
|
||||
"dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover",
|
||||
"disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
|
||||
"divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
|
||||
"donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
|
||||
"dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill",
|
||||
"drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
|
||||
"dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager",
|
||||
"eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
|
||||
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight",
|
||||
"either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
|
||||
"elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ",
|
||||
"empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
|
||||
"energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
|
||||
"enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
|
||||
"equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt",
|
||||
"escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
|
||||
"evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude",
|
||||
"excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
|
||||
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend",
|
||||
"extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
|
||||
"faith", "fall", "false", "fame", "family", "famous", "fan", "fancy",
|
||||
"fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
|
||||
"favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
|
||||
"fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
|
||||
"figure", "file", "film", "filter", "final", "find", "fine", "finger",
|
||||
"finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
|
||||
"fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight",
|
||||
"flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
|
||||
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot",
|
||||
"force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
|
||||
"foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend",
|
||||
"fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
|
||||
"fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
|
||||
"gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
|
||||
"gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius",
|
||||
"genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
|
||||
"ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass",
|
||||
"glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
|
||||
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip",
|
||||
"govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
|
||||
"gravity", "great", "green", "grid", "grief", "grit", "grocery", "group",
|
||||
"grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
|
||||
"gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
|
||||
"harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
|
||||
"head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet",
|
||||
"help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
|
||||
"hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow",
|
||||
"home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
|
||||
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble",
|
||||
"humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
|
||||
"hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill",
|
||||
"illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
|
||||
"improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
|
||||
"indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
|
||||
"inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane",
|
||||
"insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
|
||||
"invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
|
||||
"jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
|
||||
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump",
|
||||
"jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
|
||||
"key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit",
|
||||
"kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
|
||||
"lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
|
||||
"laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
|
||||
"lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave",
|
||||
"lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
|
||||
"length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty",
|
||||
"library", "license", "life", "lift", "light", "like", "limb", "limit",
|
||||
"link", "lion", "liquid", "list", "little", "live", "lizard", "load",
|
||||
"loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
|
||||
"lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber",
|
||||
"lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
|
||||
"maid", "mail", "main", "major", "make", "mammal", "man", "manage",
|
||||
"mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
|
||||
"marine", "market", "marriage", "mask", "mass", "master", "match", "material",
|
||||
"math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
|
||||
"meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory",
|
||||
"mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
|
||||
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind",
|
||||
"minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
|
||||
"mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment",
|
||||
"monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
|
||||
"mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
|
||||
"much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
|
||||
"must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin",
|
||||
"narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
|
||||
"neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral",
|
||||
"never", "news", "next", "nice", "night", "noble", "noise", "nominee",
|
||||
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice",
|
||||
"novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
|
||||
"object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean",
|
||||
"october", "odor", "off", "offer", "office", "often", "oil", "okay",
|
||||
"old", "olive", "olympic", "omit", "once", "one", "onion", "online",
|
||||
"only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
|
||||
"orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich",
|
||||
"other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
|
||||
"own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page",
|
||||
"pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
|
||||
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path",
|
||||
"patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
|
||||
"pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper",
|
||||
"perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
|
||||
"piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
|
||||
"pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
|
||||
"plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge",
|
||||
"poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
|
||||
"pool", "popular", "portion", "position", "possible", "post", "potato", "pottery",
|
||||
"poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
|
||||
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority",
|
||||
"prison", "private", "prize", "problem", "process", "produce", "profit", "program",
|
||||
"project", "promote", "proof", "property", "prosper", "protect", "proud", "provide",
|
||||
"public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
|
||||
"puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
|
||||
"pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
|
||||
"quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail",
|
||||
"rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
|
||||
"rare", "rate", "rather", "raven", "raw", "razor", "ready", "real",
|
||||
"reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
|
||||
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject",
|
||||
"relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
|
||||
"render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report",
|
||||
"require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
|
||||
"retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
|
||||
"ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
|
||||
"ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road",
|
||||
"roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
|
||||
"rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude",
|
||||
"rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
|
||||
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same",
|
||||
"sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
|
||||
"scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science",
|
||||
"scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
|
||||
"search", "season", "seat", "second", "secret", "section", "security", "seed",
|
||||
"seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
|
||||
"series", "service", "session", "settle", "setup", "seven", "shadow", "shaft",
|
||||
"shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
|
||||
"ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder",
|
||||
"shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
|
||||
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar",
|
||||
"simple", "since", "sing", "siren", "sister", "situate", "six", "size",
|
||||
"skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab",
|
||||
"slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
|
||||
"slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
|
||||
"snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
|
||||
"sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve",
|
||||
"someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
|
||||
"source", "south", "space", "spare", "spatial", "spawn", "speak", "special",
|
||||
"speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
|
||||
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray",
|
||||
"spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
|
||||
"staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay",
|
||||
"steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
|
||||
"stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
|
||||
"strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
|
||||
"submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest",
|
||||
"suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
|
||||
"sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain",
|
||||
"swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
|
||||
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table",
|
||||
"tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
|
||||
"task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten",
|
||||
"tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
|
||||
"theme", "then", "theory", "there", "they", "thing", "this", "thought",
|
||||
"three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
|
||||
"tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title",
|
||||
"toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
|
||||
"tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top",
|
||||
"topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
|
||||
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic",
|
||||
"train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
|
||||
"trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy",
|
||||
"trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
|
||||
"tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
|
||||
"twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
|
||||
"ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo",
|
||||
"unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
|
||||
"unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
|
||||
"upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
|
||||
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley",
|
||||
"valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
|
||||
"velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very",
|
||||
"vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
|
||||
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
|
||||
"vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
|
||||
"voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want",
|
||||
"warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
|
||||
"way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
|
||||
"weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
|
||||
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife",
|
||||
"wild", "will", "win", "window", "wine", "wing", "wink", "winner",
|
||||
"winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman",
|
||||
"wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
|
||||
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
|
||||
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"
|
||||
};
|
||||
171
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.c
Normal file
171
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.c
Normal file
@@ -0,0 +1,171 @@
|
||||
/* pq_crypto_firmware.c — Post-quantum crypto wrappers for ESP32 firmware.
|
||||
*
|
||||
* Wraps the PQClean algorithm API (via the pqclean component) with
|
||||
* firmware-friendly functions that handle the deterministic DRBG setup
|
||||
* for keygen and provide clean sign/verify/encaps/decaps interfaces.
|
||||
*
|
||||
* The PQ key buffers are large (ML-DSA-65 priv = 4032 bytes, SLH-DSA-128s
|
||||
* sig = 7856 bytes). Callers must allocate these on the heap or as static
|
||||
* buffers — stack allocation on ESP32 (8KB task stack default) will overflow
|
||||
* for the larger buffers.
|
||||
*/
|
||||
#include "pq_crypto_firmware.h"
|
||||
#include "pqclean.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* --- Key generation (deterministic from seed) --- */
|
||||
|
||||
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Initialize the deterministic DRBG with the mnemonic-derived seed */
|
||||
pq_drbg_init(seed, 32);
|
||||
|
||||
/* Run PQClean keygen — randombytes() draws from the DRBG */
|
||||
int ret = crypto_sign_keypair(pk, sk);
|
||||
|
||||
/* Wipe the DRBG state — the seed material is sensitive */
|
||||
pq_drbg_zeroize();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
pq_drbg_init(seed, 32);
|
||||
|
||||
/* WARNING: This call takes 5-30 seconds on ESP32 due to the
|
||||
* hypertree construction (7 layers of WOTS+ + Merkle trees). */
|
||||
int ret = slh_dsa_128s_crypto_sign_keypair(pk, sk);
|
||||
|
||||
pq_drbg_zeroize();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
pq_drbg_init(seed, 32);
|
||||
|
||||
int ret = crypto_kem_keypair(pk, sk);
|
||||
|
||||
pq_drbg_zeroize();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- Signing --- */
|
||||
|
||||
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *sk) {
|
||||
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return crypto_sign(sig, siglen, m, mlen, sk);
|
||||
}
|
||||
|
||||
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *pk) {
|
||||
if (sig == NULL || m == NULL || pk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* crypto_sign_open expects (m_out, mlen_out, sm, smlen, pk) where sm
|
||||
* is the signed message. For detached signatures we reconstruct: the
|
||||
* PQClean API uses crypto_sign_open with sm = sig || m. */
|
||||
/* For firmware use, we provide a simple verify by re-signing is not
|
||||
* possible (non-deterministic). The PQClean crypto_sign_open expects
|
||||
* the concatenated sig||msg format. Callers should use the PQClean
|
||||
* API directly for verification, or we build the sm buffer here. */
|
||||
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
|
||||
if (sm == NULL) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(sm, sig, siglen);
|
||||
memcpy(sm + siglen, m, mlen);
|
||||
|
||||
uint8_t *m_out = (uint8_t *)malloc(mlen);
|
||||
if (m_out == NULL) {
|
||||
free(sm);
|
||||
return -1;
|
||||
}
|
||||
size_t mlen_out = 0;
|
||||
|
||||
int ret = crypto_sign_open(m_out, &mlen_out, sm, siglen + mlen, pk);
|
||||
|
||||
free(sm);
|
||||
free(m_out);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *sk) {
|
||||
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* WARNING: This call takes 5-30 seconds on ESP32. */
|
||||
return slh_dsa_128s_crypto_sign(sig, siglen, m, mlen, sk);
|
||||
}
|
||||
|
||||
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *pk) {
|
||||
if (sig == NULL || m == NULL || pk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
|
||||
if (sm == NULL) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(sm, sig, siglen);
|
||||
memcpy(sm + siglen, m, mlen);
|
||||
|
||||
uint8_t *m_out = (uint8_t *)malloc(mlen);
|
||||
if (m_out == NULL) {
|
||||
free(sm);
|
||||
return -1;
|
||||
}
|
||||
size_t mlen_out = 0;
|
||||
|
||||
int ret = slh_dsa_128s_crypto_sign_open(m_out, &mlen_out, sm,
|
||||
siglen + mlen, pk);
|
||||
|
||||
free(sm);
|
||||
free(m_out);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- KEM --- */
|
||||
|
||||
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk) {
|
||||
if (ct == NULL || ss == NULL || pk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* encaps uses real randomness (hardware RNG) — the DRBG is not
|
||||
* initialized, so randombytes() falls back to esp_fill_random(). */
|
||||
return crypto_kem_enc(ct, ss, pk);
|
||||
}
|
||||
|
||||
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk) {
|
||||
if (ss == NULL || ct == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return crypto_kem_dec(ss, ct, sk);
|
||||
}
|
||||
110
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h
Normal file
110
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/* pq_crypto_firmware.h — Post-quantum crypto wrappers for ESP32 firmware.
|
||||
*
|
||||
* Provides firmware-friendly wrappers around the PQClean algorithms:
|
||||
* - ML-DSA-65 (FIPS 204 signatures)
|
||||
* - SLH-DSA-128s (FIPS 205 hash-based signatures)
|
||||
* - ML-KEM-768 (FIPS 203 key encapsulation)
|
||||
*
|
||||
* Key generation is deterministic from a 32-byte seed (derived from the
|
||||
* mnemonic via BIP-32/HMAC-SHA512). The seed feeds the deterministic DRBG
|
||||
* (pq_drbg_firmware.c) which replaces PQClean's randombytes() during keygen.
|
||||
*
|
||||
* ed25519 and x25519 are handled separately via mbedtls (see
|
||||
* key_derivation.c) and are not part of this PQClean component.
|
||||
*/
|
||||
#ifndef FIRMWARE_PQ_CRYPTO_H
|
||||
#define FIRMWARE_PQ_CRYPTO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- Algorithm identifiers --- */
|
||||
typedef enum {
|
||||
FW_PQ_ALG_ML_DSA_65 = 0,
|
||||
FW_PQ_ALG_SLH_DSA_128S,
|
||||
FW_PQ_ALG_ML_KEM_768,
|
||||
FW_PQ_ALG_UNKNOWN
|
||||
} fw_pq_alg_t;
|
||||
|
||||
/* --- Key sizes (compile-time constants, matching PQClean api.h) --- */
|
||||
#define FW_ML_DSA_65_PUBKEY_LEN 1952
|
||||
#define FW_ML_DSA_65_PRIVKEY_LEN 4032
|
||||
#define FW_ML_DSA_65_SIG_LEN 3309
|
||||
|
||||
#define FW_SLH_DSA_128S_PUBKEY_LEN 32
|
||||
#define FW_SLH_DSA_128S_PRIVKEY_LEN 64
|
||||
#define FW_SLH_DSA_128S_SIG_LEN 7856
|
||||
|
||||
#define FW_ML_KEM_768_PUBKEY_LEN 1184
|
||||
#define FW_ML_KEM_768_PRIVKEY_LEN 2400
|
||||
#define FW_ML_KEM_768_CIPHERTEXT_LEN 1088
|
||||
#define FW_ML_KEM_768_SHARED_SECRET_LEN 32
|
||||
|
||||
/* --- Key generation (deterministic from seed) --- */
|
||||
|
||||
/* Generate an ML-DSA-65 keypair from a 32-byte seed.
|
||||
* pk must be at least FW_ML_DSA_65_PUBKEY_LEN bytes.
|
||||
* sk must be at least FW_ML_DSA_65_PRIVKEY_LEN bytes.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* Generate an SLH-DSA-128s keypair from a 32-byte seed.
|
||||
* pk must be at least FW_SLH_DSA_128S_PUBKEY_LEN bytes.
|
||||
* sk must be at least FW_SLH_DSA_128S_PRIVKEY_LEN bytes.
|
||||
* Returns 0 on success, -1 on error.
|
||||
* WARNING: SLH-DSA-128s keygen takes 5-30 seconds on ESP32. */
|
||||
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* Generate an ML-KEM-768 keypair from a 32-byte seed.
|
||||
* pk must be at least FW_ML_KEM_768_PUBKEY_LEN bytes.
|
||||
* sk must be at least FW_ML_KEM_768_PRIVKEY_LEN bytes.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* --- Signing (ML-DSA-65, SLH-DSA-128s) --- */
|
||||
|
||||
/* Sign a message with ML-DSA-65.
|
||||
* sig must be at least FW_ML_DSA_65_SIG_LEN bytes.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *sk);
|
||||
|
||||
/* Verify an ML-DSA-65 signature.
|
||||
* Returns 0 on valid, -1 on invalid. */
|
||||
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *pk);
|
||||
|
||||
/* Sign a message with SLH-DSA-128s.
|
||||
* sig must be at least FW_SLH_DSA_128S_SIG_LEN bytes.
|
||||
* WARNING: SLH-DSA-128s signing takes 5-30 seconds on ESP32.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *sk);
|
||||
|
||||
/* Verify an SLH-DSA-128s signature.
|
||||
* Returns 0 on valid, -1 on invalid. */
|
||||
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
|
||||
const uint8_t *m, size_t mlen,
|
||||
const uint8_t *pk);
|
||||
|
||||
/* --- KEM (ML-KEM-768) --- */
|
||||
|
||||
/* Encapsulate: generate ciphertext + shared secret from a public key.
|
||||
* Uses real randomness (ESP32 hardware RNG) — not the deterministic DRBG.
|
||||
* ct must be at least FW_ML_KEM_768_CIPHERTEXT_LEN bytes.
|
||||
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
||||
|
||||
/* Decapsulate: recover shared secret from secret key + ciphertext.
|
||||
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
||||
|
||||
#endif /* FIRMWARE_PQ_CRYPTO_H */
|
||||
15
firmware/cyd_esp32_2432s028/main/secure_mem.c
Normal file
15
firmware/cyd_esp32_2432s028/main/secure_mem.c
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "secure_mem.h"
|
||||
|
||||
void secure_memzero(void *ptr, size_t len)
|
||||
{
|
||||
volatile unsigned char *p = (volatile unsigned char *)ptr;
|
||||
|
||||
if (p == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
*p++ = 0;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
13
firmware/cyd_esp32_2432s028/main/secure_mem.h
Normal file
13
firmware/cyd_esp32_2432s028/main/secure_mem.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void secure_memzero(void *ptr, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
315
firmware/cyd_esp32_2432s028/main/touch.c
Normal file
315
firmware/cyd_esp32_2432s028/main/touch.c
Normal file
@@ -0,0 +1,315 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "ili9341.h"
|
||||
|
||||
#define SCREEN_W 320
|
||||
#define SCREEN_H 240
|
||||
#define PREV_W 240
|
||||
#define PREV_H 320
|
||||
|
||||
#define TOUCH_PIN_CLK GPIO_NUM_25
|
||||
#define TOUCH_PIN_MOSI GPIO_NUM_32
|
||||
#define TOUCH_PIN_CS GPIO_NUM_33
|
||||
#define TOUCH_PIN_MISO GPIO_NUM_39
|
||||
#define TOUCH_PIN_IRQ GPIO_NUM_36
|
||||
#define TOUCH_SAMPLES 7
|
||||
|
||||
static const char *TAG = "touch";
|
||||
|
||||
static touch_calib_t s_cal = {
|
||||
.x_min = 281,
|
||||
.x_max = 1781,
|
||||
.y_min = 162,
|
||||
.y_max = 1725,
|
||||
.invert_x = false,
|
||||
.invert_y = true,
|
||||
};
|
||||
|
||||
static inline void touch_delay_us(int us)
|
||||
{
|
||||
esp_rom_delay_us(us);
|
||||
}
|
||||
|
||||
static inline void touch_clk_pulse(void)
|
||||
{
|
||||
gpio_set_level(TOUCH_PIN_CLK, 1);
|
||||
touch_delay_us(1);
|
||||
gpio_set_level(TOUCH_PIN_CLK, 0);
|
||||
touch_delay_us(1);
|
||||
}
|
||||
|
||||
static uint16_t xpt2046_transfer_12b(uint8_t cmd)
|
||||
{
|
||||
uint16_t value = 0;
|
||||
|
||||
gpio_set_level(TOUCH_PIN_CS, 0);
|
||||
touch_delay_us(2);
|
||||
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
gpio_set_level(TOUCH_PIN_MOSI, (cmd >> i) & 0x1);
|
||||
touch_clk_pulse();
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
gpio_set_level(TOUCH_PIN_CLK, 1);
|
||||
touch_delay_us(1);
|
||||
value = (uint16_t)((value << 1) | gpio_get_level(TOUCH_PIN_MISO));
|
||||
gpio_set_level(TOUCH_PIN_CLK, 0);
|
||||
touch_delay_us(1);
|
||||
}
|
||||
|
||||
gpio_set_level(TOUCH_PIN_CS, 1);
|
||||
touch_delay_us(2);
|
||||
|
||||
return (uint16_t)(value >> 4);
|
||||
}
|
||||
|
||||
static int map_clamped(int v, int in_min, int in_max, int out_min, int out_max)
|
||||
{
|
||||
if (v < in_min) {
|
||||
v = in_min;
|
||||
}
|
||||
if (v > in_max) {
|
||||
v = in_max;
|
||||
}
|
||||
|
||||
int den = (in_max - in_min);
|
||||
if (den == 0) {
|
||||
return out_min;
|
||||
}
|
||||
|
||||
int num = (v - in_min) * (out_max - out_min);
|
||||
return out_min + (num / den);
|
||||
}
|
||||
|
||||
static void draw_crosshair(int x, int y, uint16_t color)
|
||||
{
|
||||
const int arm = 10;
|
||||
|
||||
for (int dx = -arm; dx <= arm; ++dx) {
|
||||
int px = x + dx;
|
||||
if (px >= 0 && px < SCREEN_W && y >= 0 && y < SCREEN_H) {
|
||||
(void)ili9341_draw_pixel((uint16_t)px, (uint16_t)y, color);
|
||||
}
|
||||
}
|
||||
|
||||
for (int dy = -arm; dy <= arm; ++dy) {
|
||||
int py = y + dy;
|
||||
if (x >= 0 && x < SCREEN_W && py >= 0 && py < SCREEN_H) {
|
||||
(void)ili9341_draw_pixel((uint16_t)x, (uint16_t)py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool wait_for_stable_touch(uint16_t *out_x, uint16_t *out_y)
|
||||
{
|
||||
uint32_t sx = 0;
|
||||
uint32_t sy = 0;
|
||||
int count = 0;
|
||||
int idle_loops = 0;
|
||||
|
||||
while (count < 20) {
|
||||
uint16_t rx = 0;
|
||||
uint16_t ry = 0;
|
||||
uint16_t rz = 0;
|
||||
|
||||
if (touch_read_raw(&rx, &ry, &rz)) {
|
||||
sx += rx;
|
||||
sy += ry;
|
||||
++count;
|
||||
idle_loops = 0;
|
||||
} else {
|
||||
++idle_loops;
|
||||
if (count > 0 && idle_loops > 25) {
|
||||
sx = 0;
|
||||
sy = 0;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(8));
|
||||
}
|
||||
|
||||
*out_x = (uint16_t)(sx / count);
|
||||
*out_y = (uint16_t)(sy / count);
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t touch_init(void)
|
||||
{
|
||||
gpio_config_t out_cfg = {
|
||||
.pin_bit_mask = (1ULL << TOUCH_PIN_CLK) | (1ULL << TOUCH_PIN_MOSI) | (1ULL << TOUCH_PIN_CS),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&out_cfg));
|
||||
|
||||
gpio_config_t in_cfg = {
|
||||
.pin_bit_mask = (1ULL << TOUCH_PIN_MISO) | (1ULL << TOUCH_PIN_IRQ),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&in_cfg));
|
||||
|
||||
gpio_set_level(TOUCH_PIN_CS, 1);
|
||||
gpio_set_level(TOUCH_PIN_CLK, 0);
|
||||
gpio_set_level(TOUCH_PIN_MOSI, 0);
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"touch ready x=[%d,%d] y=[%d,%d] invert=(%d,%d)",
|
||||
s_cal.x_min,
|
||||
s_cal.x_max,
|
||||
s_cal.y_min,
|
||||
s_cal.y_max,
|
||||
s_cal.invert_x,
|
||||
s_cal.invert_y);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool touch_read_raw(uint16_t *x, uint16_t *y, uint16_t *z)
|
||||
{
|
||||
if (x == NULL || y == NULL || z == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gpio_get_level(TOUCH_PIN_IRQ) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t sx = 0;
|
||||
uint32_t sy = 0;
|
||||
uint32_t sz = 0;
|
||||
int valid = 0;
|
||||
|
||||
for (int i = 0; i < TOUCH_SAMPLES; ++i) {
|
||||
uint16_t z1 = xpt2046_transfer_12b(0xB0);
|
||||
uint16_t z2 = xpt2046_transfer_12b(0xC0);
|
||||
uint16_t pressure = (z1 > 0 && z2 > z1) ? (uint16_t)(z1 + (4095 - z2)) : 0;
|
||||
if (pressure < 80) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sx += xpt2046_transfer_12b(0xD0);
|
||||
sy += xpt2046_transfer_12b(0x90);
|
||||
sz += pressure;
|
||||
++valid;
|
||||
}
|
||||
|
||||
if (valid == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*x = (uint16_t)(sx / valid);
|
||||
*y = (uint16_t)(sy / valid);
|
||||
*z = (uint16_t)(sz / valid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool touch_read_screen(int *x, int *y, uint16_t *z)
|
||||
{
|
||||
if (x == NULL || y == NULL || z == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t raw_x = 0;
|
||||
uint16_t raw_y = 0;
|
||||
uint16_t raw_z = 0;
|
||||
|
||||
if (!touch_read_raw(&raw_x, &raw_y, &raw_z)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int old_x = map_clamped((int)raw_x,
|
||||
s_cal.x_min,
|
||||
s_cal.x_max,
|
||||
s_cal.invert_x ? (PREV_W - 1) : 0,
|
||||
s_cal.invert_x ? 0 : (PREV_W - 1));
|
||||
|
||||
int old_y = map_clamped((int)raw_y,
|
||||
s_cal.y_min,
|
||||
s_cal.y_max,
|
||||
s_cal.invert_y ? (PREV_H - 1) : 0,
|
||||
s_cal.invert_y ? 0 : (PREV_H - 1));
|
||||
|
||||
/* Keep 320x240 landscape, rotated 180° from prior orientation. */
|
||||
*x = (PREV_H - 1) - old_y;
|
||||
*y = old_x;
|
||||
|
||||
*z = raw_z;
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t touch_calibrate(void)
|
||||
{
|
||||
const int margin = 18;
|
||||
const int tx[4] = {margin, SCREEN_W - margin, SCREEN_W - margin, margin};
|
||||
const int ty[4] = {margin, margin, SCREEN_H - margin, SCREEN_H - margin};
|
||||
const char *name[4] = {"TOP-LEFT", "TOP-RIGHT", "BOTTOM-RIGHT", "BOTTOM-LEFT"};
|
||||
|
||||
uint16_t raw_x[4] = {0};
|
||||
uint16_t raw_y[4] = {0};
|
||||
|
||||
ili9341_fill_screen(ILI9341_COLOR_BLACK);
|
||||
ili9341_draw_text(6, 6, "TOUCH CAL", ILI9341_COLOR_WHITE, ILI9341_COLOR_BLACK);
|
||||
|
||||
ESP_LOGI(TAG, "Calibration start");
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ili9341_fill_screen(ILI9341_COLOR_BLACK);
|
||||
draw_crosshair(tx[i], ty[i], ILI9341_COLOR_GREEN);
|
||||
ESP_LOGI(TAG, "Tap and hold %s", name[i]);
|
||||
|
||||
wait_for_stable_touch(&raw_x[i], &raw_y[i]);
|
||||
|
||||
draw_crosshair(tx[i], ty[i], ILI9341_COLOR_BLUE);
|
||||
vTaskDelay(pdMS_TO_TICKS(350));
|
||||
}
|
||||
|
||||
int x_left = ((int)raw_x[0] + (int)raw_x[3]) / 2;
|
||||
int x_right = ((int)raw_x[1] + (int)raw_x[2]) / 2;
|
||||
int y_top = ((int)raw_y[0] + (int)raw_y[1]) / 2;
|
||||
int y_bottom = ((int)raw_y[2] + (int)raw_y[3]) / 2;
|
||||
|
||||
s_cal.x_min = (x_left < x_right) ? x_left : x_right;
|
||||
s_cal.x_max = (x_left > x_right) ? x_left : x_right;
|
||||
s_cal.y_min = (y_top < y_bottom) ? y_top : y_bottom;
|
||||
s_cal.y_max = (y_top > y_bottom) ? y_top : y_bottom;
|
||||
s_cal.invert_x = !(x_right > x_left);
|
||||
s_cal.invert_y = !(y_bottom > y_top);
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Calibration done: x=[%d,%d] y=[%d,%d] invert=(%d,%d)",
|
||||
s_cal.x_min,
|
||||
s_cal.x_max,
|
||||
s_cal.y_min,
|
||||
s_cal.y_max,
|
||||
s_cal.invert_x,
|
||||
s_cal.invert_y);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const touch_calib_t *touch_get_calibration(void)
|
||||
{
|
||||
return &s_cal;
|
||||
}
|
||||
|
||||
esp_err_t touch_set_calibration(const touch_calib_t *calib)
|
||||
{
|
||||
if (calib == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
s_cal = *calib;
|
||||
return ESP_OK;
|
||||
}
|
||||
31
firmware/cyd_esp32_2432s028/main/touch.h
Normal file
31
firmware/cyd_esp32_2432s028/main/touch.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int x_min;
|
||||
int x_max;
|
||||
int y_min;
|
||||
int y_max;
|
||||
bool invert_x;
|
||||
bool invert_y;
|
||||
} touch_calib_t;
|
||||
|
||||
esp_err_t touch_init(void);
|
||||
bool touch_read_raw(uint16_t *x, uint16_t *y, uint16_t *z);
|
||||
bool touch_read_screen(int *x, int *y, uint16_t *z);
|
||||
esp_err_t touch_calibrate(void);
|
||||
|
||||
const touch_calib_t *touch_get_calibration(void);
|
||||
esp_err_t touch_set_calibration(const touch_calib_t *calib);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
191
firmware/cyd_esp32_2432s028/main/uart_transport.c
Normal file
191
firmware/cyd_esp32_2432s028/main/uart_transport.c
Normal file
@@ -0,0 +1,191 @@
|
||||
#include "uart_transport.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define UART_PORT_NUM UART_NUM_0
|
||||
#define UART_BAUD_RATE 115200
|
||||
#define UART_RX_BUF_SIZE 4096
|
||||
#define UART_TX_BUF_SIZE 4096
|
||||
|
||||
#define UART_MAX_FRAME 4096
|
||||
#define UART_RX_RING_CAP (UART_MAX_FRAME + 32)
|
||||
|
||||
typedef struct {
|
||||
uint8_t buf[UART_RX_RING_CAP];
|
||||
size_t have;
|
||||
} rx_ring_t;
|
||||
|
||||
static const char *TAG = "uart_transport";
|
||||
|
||||
static bool s_inited = false;
|
||||
static rx_ring_t s_uart_rx;
|
||||
|
||||
static int parse_frame_from_ring(rx_ring_t *ring, uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
if (ring == NULL || payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ring->have < 4) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint32_t len = ((uint32_t)ring->buf[0] << 24) |
|
||||
((uint32_t)ring->buf[1] << 16) |
|
||||
((uint32_t)ring->buf[2] << 8) |
|
||||
(uint32_t)ring->buf[3];
|
||||
|
||||
if (len == 0 || len > payload_max) {
|
||||
memmove(ring->buf, ring->buf + 1, ring->have - 1);
|
||||
ring->have -= 1;
|
||||
if (ring->have < 4) {
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ring->have < (size_t)len + 4U) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
memcpy(payload, ring->buf + 4, len);
|
||||
*out_len = (size_t)len;
|
||||
|
||||
size_t rem = ring->have - ((size_t)len + 4U);
|
||||
if (rem > 0) {
|
||||
memmove(ring->buf, ring->buf + 4U + len, rem);
|
||||
}
|
||||
ring->have = rem;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void read_into_ring(rx_ring_t *ring, const uint8_t *data, size_t n)
|
||||
{
|
||||
if (ring == NULL || data == NULL || n == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ring->have + n > sizeof(ring->buf)) {
|
||||
ring->have = 0;
|
||||
}
|
||||
|
||||
memcpy(ring->buf + ring->have, data, n);
|
||||
ring->have += n;
|
||||
}
|
||||
|
||||
int uart_transport_init(void)
|
||||
{
|
||||
if (s_inited) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uart_config_t uart_cfg = {
|
||||
.baud_rate = UART_BAUD_RATE,
|
||||
.data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
.source_clk = UART_SCLK_DEFAULT,
|
||||
};
|
||||
|
||||
if (uart_driver_install(UART_PORT_NUM, UART_RX_BUF_SIZE, UART_TX_BUF_SIZE, 0, NULL, 0) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "uart_driver_install failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (uart_param_config(UART_PORT_NUM, &uart_cfg) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "uart_param_config failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (uart_set_pin(UART_PORT_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "uart_set_pin failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&s_uart_rx, 0, sizeof(s_uart_rx));
|
||||
s_inited = true;
|
||||
|
||||
ESP_LOGI(TAG, "UART transport initialized on UART0 @ %d", UART_BAUD_RATE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int usb_transport_init(void)
|
||||
{
|
||||
return uart_transport_init();
|
||||
}
|
||||
|
||||
int cdc_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
uint8_t tmp[128];
|
||||
|
||||
if (payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!s_inited) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int n = uart_read_bytes(UART_PORT_NUM, tmp, sizeof(tmp), 0);
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
read_into_ring(&s_uart_rx, tmp, (size_t)n);
|
||||
}
|
||||
|
||||
return parse_frame_from_ring(&s_uart_rx, payload, payload_max, out_len);
|
||||
}
|
||||
|
||||
int cdc_send_frame(const uint8_t *payload, size_t len)
|
||||
{
|
||||
uint8_t hdr[4];
|
||||
|
||||
if (!s_inited || payload == NULL || len == 0 || len > UINT_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
hdr[0] = (uint8_t)((len >> 24) & 0xFFU);
|
||||
hdr[1] = (uint8_t)((len >> 16) & 0xFFU);
|
||||
hdr[2] = (uint8_t)((len >> 8) & 0xFFU);
|
||||
hdr[3] = (uint8_t)(len & 0xFFU);
|
||||
|
||||
int wrote = uart_write_bytes(UART_PORT_NUM, (const char *)hdr, sizeof(hdr));
|
||||
if (wrote != (int)sizeof(hdr)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
wrote = uart_write_bytes(UART_PORT_NUM, (const char *)payload, len);
|
||||
if (wrote != (int)len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (uart_wait_tx_done(UART_PORT_NUM, pdMS_TO_TICKS(2000)) != ESP_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
(void)payload;
|
||||
(void)payload_max;
|
||||
(void)out_len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len)
|
||||
{
|
||||
(void)payload;
|
||||
(void)len;
|
||||
return -1;
|
||||
}
|
||||
21
firmware/cyd_esp32_2432s028/main/uart_transport.h
Normal file
21
firmware/cyd_esp32_2432s028/main/uart_transport.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int uart_transport_init(void);
|
||||
|
||||
int cdc_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int cdc_send_frame(const uint8_t *payload, size_t len);
|
||||
|
||||
/* CYD has no native USB/WebUSB. Keep these symbols for feather main.c parity. */
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1082
firmware/cyd_esp32_2432s028/main/ui.c
Normal file
1082
firmware/cyd_esp32_2432s028/main/ui.c
Normal file
File diff suppressed because it is too large
Load Diff
27
firmware/cyd_esp32_2432s028/main/ui.h
Normal file
27
firmware/cyd_esp32_2432s028/main/ui.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
UI_APPROVAL_DENY = 0,
|
||||
UI_APPROVAL_ONCE = 1,
|
||||
UI_APPROVAL_ALWAYS = 2,
|
||||
UI_APPROVAL_TIMEOUT = 3,
|
||||
} ui_approval_decision_t;
|
||||
|
||||
int ui_run_until_working(char out_mnemonic[256]);
|
||||
int ui_init_runtime(void);
|
||||
void ui_pump(void);
|
||||
|
||||
void ui_show_approval_prompt(const char *caller_pubkey_hex, int kind, const char *content, uint32_t timeout_ms);
|
||||
ui_approval_decision_t ui_wait_for_approval(uint32_t timeout_ms);
|
||||
void ui_show_idle(const char *npub_full, const char *mnemonic_full, const char *version_text);
|
||||
void ui_add_event(const char *action, const char *caller_pubkey_hex, const char *extra);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
20
firmware/cyd_esp32_2432s028/main/usb_transport.h
Normal file
20
firmware/cyd_esp32_2432s028/main/usb_transport.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int usb_transport_init(void);
|
||||
|
||||
int cdc_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int cdc_send_frame(const uint8_t *payload, size_t len);
|
||||
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
codecov:
|
||||
notify:
|
||||
require_ci_to_pass: true
|
||||
comment: off
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
project: off
|
||||
@@ -0,0 +1 @@
|
||||
d7c1ac037ae6e85d94897f807d6e7ba0946a83e720074fc95a4f6241da9f9f53
|
||||
@@ -0,0 +1,6 @@
|
||||
[*.{c,h,ino}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
27
firmware/cyd_esp32_2432s028/managed_components/lvgl__lvgl/.gitignore
vendored
Normal file
27
firmware/cyd_esp32_2432s028/managed_components/lvgl__lvgl/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
**/*.o
|
||||
**/*bin
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
**/*.gcda
|
||||
**/*.gcno
|
||||
tags
|
||||
docs/api_doc
|
||||
scripts/cppcheck_res.txt
|
||||
scripts/built_in_font/lv_font_*
|
||||
docs/doxygen_html
|
||||
docs/xml
|
||||
docs/examples.md
|
||||
docs/out_latex
|
||||
docs/_static/built_lv_examples
|
||||
docs/LVGL.pdf
|
||||
docs/env
|
||||
out_html
|
||||
__pycache__
|
||||
/emscripten_builder
|
||||
test_screenshot_error.h
|
||||
build/
|
||||
tests/build_*/
|
||||
tests/report/
|
||||
.DS_Store
|
||||
.vscode
|
||||
*.bak
|
||||
@@ -0,0 +1,30 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
# Run astyle over the staged files with c and h extension found in the directories
|
||||
# listed in the files regex pattern. Ignoring the files in the exclude pattern.
|
||||
- id: format-source
|
||||
name: Formatting source files
|
||||
entry: astyle --options=scripts/code-format.cfg --ignore-exclude-errors
|
||||
stages: [ commit ]
|
||||
language: system
|
||||
pass_filenames: true
|
||||
verbose: true
|
||||
files: |
|
||||
(?x)^(
|
||||
src/ |
|
||||
tests/src/test_cases/
|
||||
)
|
||||
exclude: |
|
||||
(?x)^(
|
||||
src/extra/libs/ |
|
||||
src/lv_conf_internal.h
|
||||
)
|
||||
types_or: ["c", "header"]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.12.4)
|
||||
|
||||
if(NOT ESP_PLATFORM)
|
||||
project(lvgl HOMEPAGE_URL https://github.com/lvgl/lvgl)
|
||||
endif()
|
||||
|
||||
set(LVGL_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
if(ESP_PLATFORM)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/esp.cmake)
|
||||
elseif(ZEPHYR_BASE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/zephyr.cmake)
|
||||
elseif(MICROPY_DIR)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/micropython.cmake)
|
||||
else()
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/custom.cmake)
|
||||
endif()
|
||||
1139
firmware/cyd_esp32_2432s028/managed_components/lvgl__lvgl/Kconfig
Normal file
1139
firmware/cyd_esp32_2432s028/managed_components/lvgl__lvgl/Kconfig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
MIT licence
|
||||
Copyright (c) 2021 LVGL Kft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,186 @@
|
||||
<h1 align="center"> LVGL - Light and Versatile Graphics Library</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://lvgl.io/assets/images/lvgl_widgets_demo.gif">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
LVGL provides everything you need to create an embedded GUI with easy-to-use graphical elements, beautiful visual effects and a low memory footprint.
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://lvgl.io">Website </a> ·
|
||||
<a href="https://docs.lvgl.io/">Docs</a> ·
|
||||
<a href="https://forum.lvgl.io">Forum</a> ·
|
||||
<a href="https://lvgl.io/services">Services</a> ·
|
||||
<a href="https://docs.lvgl.io/master/examples.html">Interactive examples</a>
|
||||
</h4>
|
||||
|
||||
|
||||
**English** | [中文](./README_zh.md) | [Português do Brasil](./README_pt_BR.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### Table of content
|
||||
- [Overview](#overview)
|
||||
- [Get started](#get-started)
|
||||
- [Examples](#examples)
|
||||
- [Services](#services)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Overview
|
||||
### Features
|
||||
* Powerful [building blocks](https://docs.lvgl.io/master/widgets/index.html): buttons, charts, lists, sliders, images, etc.
|
||||
* Advanced graphics engine: animations, anti-aliasing, opacity, smooth scrolling, blending modes, etc
|
||||
* Supports [various input devices](https://docs.lvgl.io/master/overview/indev.html): touchscreen, mouse, keyboard, encoder, buttons, etc.
|
||||
* Supports [multiple displays](https://docs.lvgl.io/master/overview/display.html)
|
||||
* Hardware independent, can be use with any microcontroller and display
|
||||
* Scalable to operate with little memory (64 kB Flash, 16 kB RAM)
|
||||
* Multi-language support with UTF-8 handling, CJK, Bidirectional and Arabic script support
|
||||
* Fully customizable graphical elements via [CSS-like styles](https://docs.lvgl.io/master/overview/style.html)
|
||||
* Powerful layouts inspired by CSS: [Flexbox](https://docs.lvgl.io/master/layouts/flex.html) and [Grid](https://docs.lvgl.io/master/layouts/grid.html)
|
||||
* OS, External memory and GPU are supported but not required. (built in support for STM32 DMA2D, SWM341 DMA2D, and NXP PXP and VGLite)
|
||||
* Smooth rendering even with a [single frame buffer](https://docs.lvgl.io/master/porting/display.html)
|
||||
* Written in C and compatible with C++
|
||||
* Micropython Binding exposes [LVGL API in Micropython](https://blog.lvgl.io/2019-02-20/micropython-bindings)
|
||||
* [Simulator](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html) to develop on PC without embedded hardware
|
||||
* 100+ simple [Examples](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
* [Documentation](http://docs.lvgl.io/) and API references online and in PDF
|
||||
|
||||
### Requirements
|
||||
Basically, every modern controller (which is able to drive a display) is suitable to run LVGL. The minimal requirements are:
|
||||
<table>
|
||||
<tr>
|
||||
<td> <strong>Name</strong> </td>
|
||||
<td><strong>Minimal</strong></td>
|
||||
<td><strong>Recommended</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Architecture</strong></td>
|
||||
<td colspan="2">16, 32 or 64 bit microcontroller or processor</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <strong>Clock</strong></td>
|
||||
<td> > 16 MHz </td>
|
||||
<td> > 48 MHz</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Flash/ROM</strong></td>
|
||||
<td> > 64 kB </td>
|
||||
<td> > 180 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Static RAM</strong></td>
|
||||
<td> > 16 kB </td>
|
||||
<td> > 48 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Draw buffer</strong></td>
|
||||
<td> > 1 × <em>hor. res.</em> pixels </td>
|
||||
<td> > 1/10 screen size </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Compiler</strong></td>
|
||||
<td colspan="2"> C99 or newer </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*Note that the memory usage might vary depending on the architecture, compiler and build options.*
|
||||
|
||||
### Supported platforms
|
||||
LVGL is completely platform independent and can be used with any MCU that fulfills the requirements.
|
||||
Just to mention some platforms:
|
||||
- NXP: Kinetis, LPC, iMX, iMX RT
|
||||
- STM32F1, STM32F3, STM32F4, STM32F7, STM32L4, STM32L5, STM32H7
|
||||
- Microchip dsPIC33, PIC24, PIC32MX, PIC32MZ
|
||||
- [Linux frame buffer](https://blog.lvgl.io/2018-01-03/linux_fb) (/dev/fb)
|
||||
- [Raspberry Pi](https://github.com/lvgl/lv_port_linux_frame_buffer)
|
||||
- [Espressif ESP32](https://github.com/lvgl/lv_port_esp32)
|
||||
- [Infineon Aurix](https://github.com/lvgl/lv_port_aurix)
|
||||
- Nordic NRF52 Bluetooth modules
|
||||
- Quectel modems
|
||||
- [SYNWIT SWM341](http://www.synwit.cn/)
|
||||
|
||||
LVGL is also available as:
|
||||
- [Arduino library](https://docs.lvgl.io/master/get-started/platforms/arduino.html)
|
||||
- [PlatformIO package](https://registry.platformio.org/libraries/lvgl/lvgl)
|
||||
- [Zephyr library](https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_LVGL.html)
|
||||
- [ESP32 component](https://docs.lvgl.io/master/get-started/platforms/espressif.html)
|
||||
- [NXP MCUXpresso component](https://www.nxp.com/design/software/embedded-software/lvgl-open-source-graphics-library:LITTLEVGL-OPEN-SOURCE-GRAPHICS-LIBRARY)
|
||||
- [NuttX library](https://docs.lvgl.io/master/get-started/os/nuttx.html)
|
||||
- [RT-Thread RTOS](https://docs.lvgl.io/master/get-started/os/rt-thread.html)
|
||||
|
||||
|
||||
## Get started
|
||||
This list shows the recommended way of learning the library:
|
||||
1. Check the [Online demos](https://lvgl.io/demos) to see LVGL in action (3 minutes)
|
||||
2. Read the [Introduction](https://docs.lvgl.io/master/intro/index.html) page of the documentation (5 minutes)
|
||||
3. Get familiar with the basics on the [Quick overview](https://docs.lvgl.io/master/get-started/quick-overview.html) page (15 minutes)
|
||||
4. Set up a [Simulator](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html) (10 minutes)
|
||||
5. Try out some [Examples](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
6. Port LVGL to a board. See the [Porting](https://docs.lvgl.io/master/porting/index.html) guide or check the ready to use [Projects](https://github.com/lvgl?q=lv_port_)
|
||||
7. Read the [Overview](https://docs.lvgl.io/master/overview/index.html) page to get a better understanding of the library (2-3 hours)
|
||||
8. Check the documentation of the [Widgets](https://docs.lvgl.io/master/widgets/index.html) to see their features and usage
|
||||
9. If you have questions go to the [Forum](http://forum.lvgl.io/)
|
||||
10. Read the [Contributing](https://docs.lvgl.io/master/CONTRIBUTING.html) guide to see how you can help to improve LVGL (15 minutes)
|
||||
|
||||
## Examples
|
||||
|
||||
For more examples see the [examples](https://github.com/lvgl/lvgl/tree/master/examples) folder.
|
||||
|
||||

|
||||
|
||||
### C
|
||||
```c
|
||||
lv_obj_t * btn = lv_btn_create(lv_scr_act()); /*Add a button to the current screen*/
|
||||
lv_obj_set_pos(btn, 10, 10); /*Set its position*/
|
||||
lv_obj_set_size(btn, 100, 50); /*Set its size*/
|
||||
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); /*Assign a callback to the button*/
|
||||
|
||||
lv_obj_t * label = lv_label_create(btn); /*Add a label to the button*/
|
||||
lv_label_set_text(label, "Button"); /*Set the labels text*/
|
||||
lv_obj_center(label); /*Align the label to the center*/
|
||||
...
|
||||
|
||||
void btn_event_cb(lv_event_t * e)
|
||||
{
|
||||
printf("Clicked\n");
|
||||
}
|
||||
```
|
||||
### Micropython
|
||||
Learn more about [Micropython](https://docs.lvgl.io/master/get-started/bindings/micropython.html).
|
||||
```python
|
||||
def btn_event_cb(e):
|
||||
print("Clicked")
|
||||
|
||||
# Create a Button and a Label
|
||||
btn = lv.btn(lv.scr_act())
|
||||
btn.set_pos(10, 10)
|
||||
btn.set_size(100, 50)
|
||||
btn.add_event_cb(btn_event_cb, lv.EVENT.CLICKED, None)
|
||||
|
||||
label = lv.label(btn)
|
||||
label.set_text("Button")
|
||||
label.center()
|
||||
```
|
||||
|
||||
## Services
|
||||
LVGL Kft was established to provide a solid background for LVGL library. We offer several type of services to help you in UI development:
|
||||
- Graphics design
|
||||
- UI implementation
|
||||
- Consulting/Support
|
||||
|
||||
For more information see https://lvgl.io/services
|
||||
Feel free to contact us if you have any questions.
|
||||
|
||||
|
||||
## Contributing
|
||||
LVGL is an open project and contribution is very welcome. There are many ways to contribute from simply speaking about your project, through writing examples, improving the documentation, fixing bugs to hosting your own project under the LVGL organization.
|
||||
|
||||
For a detailed description of contribution opportunities visit the [Contributing](https://docs.lvgl.io/master/CONTRIBUTING.html) section of the documentation.
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<h1 align="center"> LVGL - Biblioteca gráfica leve e versátil</h1>
|
||||
<p align="center">
|
||||
<img src="https://lvgl.io/assets/images/lvgl_widgets_demo.gif">
|
||||
</p>
|
||||
<p align="center">
|
||||
O LVGL fornece tudo o que você precisa para criar uma GUI incorporada com elementos gráficos fáceis de usar, belos efeitos visuais e um baixo consumo de memória.
|
||||
</p>
|
||||
<h4 align="center">
|
||||
<a href="https://lvgl.io">Site</a> ·
|
||||
<a href="https://docs.lvgl.io/">Documentação</a> ·
|
||||
<a href="https://forum.lvgl.io">Fórum</a> ·
|
||||
<a href="https://lvgl.io/services">Serviços</a> ·
|
||||
<a href="https://docs.lvgl.io/master/examples.html">Exemplos interativos</a>
|
||||
</h4>
|
||||
|
||||
[English](./README.md) | [中文](./README_zh.md) | **Português do Brasil**
|
||||
|
||||
---
|
||||
|
||||
### Tabela de conteúdo
|
||||
|
||||
- [Visão Geral](#visão-geral)
|
||||
- [Iniciando](#iniciando)
|
||||
- [Exemplos](#exemplos)
|
||||
- [Serviços](#serviços)
|
||||
- [Contribuindo](#contribuindo)
|
||||
|
||||
## Visão Geral
|
||||
|
||||
### Recursos
|
||||
* Poderosos [widgets](https://docs.lvgl.io/master/widgets/index.html): botões, gráficos, listas, controles deslizantes (sliders), imagens, etc.
|
||||
* Mecanismo gráfico avançado: animações, anti-aliasing, opacidade, rolagem suave, modos de mesclagem (blending modes), etc.
|
||||
* Suporte à [vários dispositivos de entrada](https://docs.lvgl.io/master/overview/indev.html): tela sensível ao toque, mouse, teclado, codificador, botões, etc.
|
||||
* Suporte à [vários monitores](https://docs.lvgl.io/master/overview/display.html)
|
||||
* Pode ser usado com qualquer microcontrolador e display, independente do hardware
|
||||
* Escalável para operar com pouca memória (64 kB Flash, 16 kB RAM)
|
||||
* Suporte multilíngue com manipulação UTF-8, suporte ao alfabeto bidirecional, árabe e CJK (Chinês, Japonês e Coreano)
|
||||
* Elementos gráficos totalmente personalizáveis por meio de [CSS](https://docs.lvgl.io/master/overview/style.html)
|
||||
* Layouts poderosos inspirados em CSS: [Flexbox](https://docs.lvgl.io/master/layouts/flex.html) e [Grid](https://docs.lvgl.io/master/layouts/grid.html)
|
||||
* SO, memória externa e GPU são suportados, mas não obrigatórios. (suporte integrado para STM32 DMA2D, SWM341 DMA2D e NXP PXP e VGLite)
|
||||
* Renderização suave mesmo com um [buffer de quadro único](https://docs.lvgl.io/master/porting/display.html) (single frame buffer)
|
||||
* Escrito em C e compatível com C++
|
||||
* Uso do LittlevGL com Micropython simplificado com [LVGL API in Micropython](https://blog.lvgl.io/2019-02-20/micropython-bindings)
|
||||
* [Simulador](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html) para desenvolver no PC sem hardware embutido
|
||||
* Mais de 100 [exemplos simples](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
* [Documentação](http://docs.lvgl.io/) e referências de API online e em PDF
|
||||
|
||||
### Requerimentos
|
||||
Basicamente, todo controlador moderno (que é capaz de acionar um display) é adequado para executar LVGL. Os requisitos mínimos são:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Nome</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Minímo</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Recomendado</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Arquitetura</strong>
|
||||
</td>
|
||||
<td colspan="2">Microcontrolador ou processador de 16, 32 ou 64 bits</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Clock</strong>
|
||||
</td>
|
||||
<td>> 16 MHz</td>
|
||||
<td>> 48 MHz</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Flash/ROM</strong>
|
||||
</td>
|
||||
<td>> 64 kB</td>
|
||||
<td>> 180 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>RAM estática</strong>
|
||||
</td>
|
||||
<td>> 16 kB</td>
|
||||
<td>> 48 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Draw buffer</strong>
|
||||
</td>
|
||||
<td>> 1 × <em>hor. res.</em> pixels</td>
|
||||
<td>> tamanho da tela de 1/10</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Compilador</strong>
|
||||
</td>
|
||||
<td colspan="2">Padrão C99 ou mais recente</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*Observe que o uso de memória pode variar dependendo da arquitetura, do compilador e das opções de compilação.*
|
||||
|
||||
### Plataformas suportadas
|
||||
O LVGL é completamente independente de plataforma e pode ser usado com qualquer MCU que atenda aos requisitos.
|
||||
Apenas para citar algumas plataformas:
|
||||
|
||||
- NXP: Kinetis, LPC, iMX, iMX RT
|
||||
- STM32F1, STM32F3, STM32F4, STM32F7, STM32L4, STM32L5, STM32H7
|
||||
- Microchip dsPIC33, PIC24, PIC32MX, PIC32MZ
|
||||
- [Linux frame buffer](https://blog.lvgl.io/2018-01-03/linux_fb) (/dev/fb)
|
||||
- [Raspberry Pi](http://www.vk3erw.com/index.php/16-software/63-raspberry-pi-official-7-touchscreen-and-littlevgl)
|
||||
- [Espressif ESP32](https://github.com/lvgl/lv_port_esp32)
|
||||
- [Infineon Aurix](https://github.com/lvgl/lv_port_aurix)
|
||||
- Nordic NRF52 Bluetooth modules
|
||||
- Quectel modems
|
||||
- [SYNWIT SWM341](https://www.synwit.cn/)
|
||||
|
||||
LVGL também está disponível para:
|
||||
- [Arduino library](https://docs.lvgl.io/master/get-started/platforms/arduino.html)
|
||||
- [PlatformIO package](https://registry.platformio.org/libraries/lvgl/lvgl)
|
||||
- [Zephyr library](https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_LVGL.html)
|
||||
- [ESP32 component](https://docs.lvgl.io/master/get-started/platforms/espressif.html)
|
||||
- [NXP MCUXpresso component](https://www.nxp.com/design/software/embedded-software/lvgl-open-source-graphics-library:LITTLEVGL-OPEN-SOURCE-GRAPHICS-LIBRARY)
|
||||
- [NuttX library](https://docs.lvgl.io/master/get-started/os/nuttx.html)
|
||||
- [RT-Thread RTOS](https://docs.lvgl.io/master/get-started/os/rt-thread.html)
|
||||
|
||||
## Iniciando
|
||||
Esta lista mostra a maneira recomendada de aprender sobre a biblioteca:
|
||||
|
||||
1. Confira as [demos on-line](https://lvgl.io/demos) para ver o LVGL em ação (3 minutos)
|
||||
2. Leia a [introdução](https://docs.lvgl.io/master/intro/index.html) da documentação (5 minutos)
|
||||
3. Familiarize-se com o básico da [Visão geral rápida](https://docs.lvgl.io/master/get-started/quick-overview.html) (15 minutos)
|
||||
4. Configure um [simulador](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html) (10 minutos)
|
||||
5. Experimente alguns [Exemplos](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
6. Placa para porta LVGL. Veja o guia [porting](https://docs.lvgl.io/master/porting/index.html) ou verifique o pronto para usar [Projects](https://github.com/lvgl?q=lv_port_)
|
||||
7. Leia a [visão geral](https://docs.lvgl.io/master/overview/index.html) para entender melhor a biblioteca (2-3 horas)
|
||||
8. Verifique a documentação dos [widgets](https://docs.lvgl.io/master/widgets/index.html) para ver seus recursos e como utilizá-los
|
||||
9. Se você tiver dúvidas, acesse o [fórum](http://forum.lvgl.io/)
|
||||
10. Leia o guia de [contribuição](https://docs.lvgl.io/master/CONTRIBUTING.html) para ver como você pode ajudar a melhorar o LVGL (15 minutos)
|
||||
|
||||
## Exemplos
|
||||
Para mais exemplos, veja a pasta [examples](https://github.com/lvgl/lvgl/tree/master/examples).
|
||||
|
||||

|
||||
|
||||
### C
|
||||
|
||||
```c
|
||||
lv_obj_t * button = lv_btn_create(lv_scr_act()); /* Adiciona um botão à tela atual */
|
||||
lv_obj_set_pos(button, 10, 10); /* Define uma posição ao botão na tela */
|
||||
lv_obj_set_size(button, 100, 50); /* Define o tamanho */
|
||||
lv_obj_add_event_cb(button, button_event_callback, LV_EVENT_CLICKED, NULL); /* Atribui um retorno de chamada (callback) */
|
||||
|
||||
lv_obj_t * label = lv_label_create(button); /* Adiciona um rótulo (label) */
|
||||
lv_label_set_text(label, "Clique aqui"); /* Define o texto do rótulo (label) */
|
||||
lv_obj_center(label); /* Alinha o texto ao centro */
|
||||
...
|
||||
|
||||
void button_event_callback(lv_event_t * e)
|
||||
{
|
||||
printf("Clicado\n");
|
||||
}
|
||||
```
|
||||
|
||||
### Micropython
|
||||
Saiba mais em [Micropython](https://docs.lvgl.io/master/get-started/bindings/micropython.html)
|
||||
|
||||
```python
|
||||
def button_event_callback(event):
|
||||
print("Clicado")
|
||||
|
||||
# Cria um botão e um rótulo (label)
|
||||
button = lv.btn(lv.scr_act())
|
||||
button.set_pos(10, 10)
|
||||
button.set_size(100, 50)
|
||||
button.add_event_cb(button_event_callback, lv.EVENT.CLICKED, None)
|
||||
|
||||
label = lv.label(button)
|
||||
label.set_text("Cliquq aqui")
|
||||
label.center()
|
||||
```
|
||||
|
||||
## Serviços
|
||||
O LVGL Kft foi estabelecido para fornecer uma base sólida para a biblioteca LVGL. Oferecemos vários tipos de serviços
|
||||
para ajudá-lo no desenvolvimento da interface do usuário:
|
||||
|
||||
- Design gráfico
|
||||
- Implementação de IU
|
||||
- Consultoria/Suporte
|
||||
|
||||
Para mais informações, consulte [LVGL Serviços](https://lvgl.io/services). Sinta-se à vontade para entrar em contato
|
||||
conosco se tiver alguma dúvida.
|
||||
|
||||
## Contribuindo
|
||||
O LVGL é um projeto aberto e sua contribuição é muito bem-vinda. Há muitas maneiras de contribuir, desde simplesmente
|
||||
falando sobre seu projeto, escrevendo exemplos, melhorando a documentação, corrigindo bugs até hospedar seu próprio
|
||||
projeto sob a organização LVGL.
|
||||
|
||||
Para obter uma descrição detalhada das oportunidades de contribuição, visite a seção de [contribuição](https://docs.lvgl.io/master/CONTRIBUTING.html) da documentação.
|
||||
@@ -0,0 +1,193 @@
|
||||
<h1 align="center"> LVGL - Light and Versatile Graphics Library</h1>
|
||||
<h2 align="center"> LVGL - 轻量级通用型图形库</h2>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://lvgl.io/assets/images/lvgl_widgets_demo.gif">
|
||||
</p>
|
||||
<p align="center">
|
||||
LVGL是一个高度可裁剪、低资源占用、界面美观且易用的嵌入式系统图形库
|
||||
</p>
|
||||
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://lvgl.io">官网</a> ·
|
||||
<a href="https://docs.lvgl.io/">文档</a> ·
|
||||
<a href="https://forum.lvgl.io">论坛</a> ·
|
||||
<a href="https://lvgl.io/services">服务</a> ·
|
||||
<a href="https://docs.lvgl.io/master/examples.html">例程</a>
|
||||
</h4>
|
||||
|
||||
|
||||
[English](./README.md) | **中文** | [Português do Brasil](./README_pt_BR.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 目录
|
||||
- [概况与总览](#概况与总览)
|
||||
- [如何入门](#如何入门)
|
||||
- [例程](#例程)
|
||||
- [服务](#服务)
|
||||
- [如何向社区贡献](#如何向社区贡献)
|
||||
|
||||
## 概况与总览
|
||||
### 特性
|
||||
* 丰富且强大的模块化[图形组件](https://docs.lvgl.io/master/widgets/index.html):按钮 (buttons)、图表 (charts)、列表 (lists)、滑动条 (sliders)、图片 (images) 等
|
||||
* 高级的图形引擎:动画、抗锯齿、透明度、平滑滚动、图层混合等效果
|
||||
* 支持多种[输入设备](https://docs.lvgl.io/master/overview/indev.html):触摸屏、 键盘、编码器、按键等
|
||||
* 支持[多显示设备](https://docs.lvgl.io/master/overview/display.html)
|
||||
* 不依赖特定的硬件平台,可以在任何显示屏上运行
|
||||
* 配置可裁剪(最低资源占用:64 kB Flash,16 kB RAM)
|
||||
* 基于UTF-8的多语种支持,例如中文、日文、韩文、阿拉伯文等
|
||||
* 可以通过[类CSS](https://docs.lvgl.io/master/overview/style.html)的方式来设计、布局图形界面(例如:[Flexbox](https://docs.lvgl.io/master/layouts/flex.html)、[Grid](https://docs.lvgl.io/master/layouts/grid.html))
|
||||
* 支持操作系统、外置内存、以及硬件加速(LVGL已内建支持STM32 DMA2D、SWM341 DMA2D、NXP PXP和VGLite)
|
||||
* 即便仅有[单缓冲区(frame buffer)](https://docs.lvgl.io/master/porting/display.html)的情况下,也可保证渲染如丝般顺滑
|
||||
* 全部由C编写完成,并支持C++调用
|
||||
* 支持Micropython编程,参见:[LVGL API in Micropython](https://blog.lvgl.io/2019-02-20/micropython-bindings)
|
||||
* 支持[模拟器](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html)仿真,可以无硬件依托进行开发
|
||||
* 丰富详实的[例程](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
* 详尽的[文档](http://docs.lvgl.io/)以及API参考手册,可线上查阅或可下载为PDF格式
|
||||
|
||||
### 硬件要求
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td> <strong>要求</strong> </td>
|
||||
<td><strong>最低要求</strong></td>
|
||||
<td><strong>建议要求</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>架构</strong></td>
|
||||
<td colspan="2">16、32、64位微控制器或微处理器</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <strong>时钟</strong></td>
|
||||
<td> > 16 MHz</td>
|
||||
<td> > 48 MHz</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Flash/ROM</strong></td>
|
||||
<td> > 64 kB </td>
|
||||
<td> > 180 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Static RAM</strong></td>
|
||||
<td> > 16 kB </td>
|
||||
<td> > 48 kB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>Draw buffer</strong></td>
|
||||
<td> > 1 × <em>hor. res.</em> pixels </td>
|
||||
<td> > 1/10屏幕大小 </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> <strong>编译器</strong></td>
|
||||
<td colspan="2"> C99或更新 </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*注意:资源占用情况与具体硬件平台、编译器等因素有关,上表中仅给出参考值*
|
||||
|
||||
### 已经支持的平台
|
||||
LVGL本身并不依赖特定的硬件平台,任何满足LVGL硬件配置要求的微控制器均可运行LVGL。
|
||||
如下仅列举其中一部分:
|
||||
|
||||
- NXP: Kinetis, LPC, iMX, iMX RT
|
||||
- STM32F1, STM32F3, STM32F4, STM32F7, STM32L4, STM32L5, STM32H7
|
||||
- Microchip dsPIC33, PIC24, PIC32MX, PIC32MZ
|
||||
- [Linux frame buffer](https://blog.lvgl.io/2018-01-03/linux_fb) (/dev/fb)
|
||||
- [Raspberry Pi](http://www.vk3erw.com/index.php/16-software/63-raspberry-pi-official-7-touchscreen-and-littlevgl)
|
||||
- [Espressif ESP32](https://github.com/lvgl/lv_port_esp32)
|
||||
- [Infineon Aurix](https://github.com/lvgl/lv_port_aurix)
|
||||
- Nordic NRF52 Bluetooth modules
|
||||
- Quectel modems
|
||||
- [SYNWIT SWM341](https://www.synwit.cn/)
|
||||
|
||||
LVGL也支持:
|
||||
- [Arduino library](https://docs.lvgl.io/master/get-started/platforms/arduino.html)
|
||||
- [PlatformIO package](https://platformio.org/lib/show/12440/lvgl)
|
||||
- [Zephyr library](https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_LVGL.html)
|
||||
- [ESP32 component](https://docs.lvgl.io/master/get-started/platforms/espressif.html)
|
||||
- [NXP MCUXpresso component](https://www.nxp.com/design/software/embedded-software/lvgl-open-source-graphics-library:LITTLEVGL-OPEN-SOURCE-GRAPHICS-LIBRARY)
|
||||
- [NuttX library](https://docs.lvgl.io/master/get-started/os/nuttx.html)
|
||||
- [RT-Thread RTOS](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/packages-manual/lvgl-docs/introduction)
|
||||
|
||||
|
||||
## 如何入门
|
||||
请按照如下顺序来学习LVGL:
|
||||
1. 使用[网页在线例程](https://lvgl.io/demos)来体验LVGL(3分钟)
|
||||
2. 阅读文档[简介](https://docs.lvgl.io/master/intro/index.html)章节来初步了解LVGL(5分钟)
|
||||
3. 再来阅读一下文档快速[快速概览](https://docs.lvgl.io/master/get-started/quick-overview.html)章节来了解LVGL的基本知识(15分钟)
|
||||
4. 学习如何使用[模拟器](https://docs.lvgl.io/master/get-started/platforms/pc-simulator.html)来在电脑上仿真LVGL(10分钟)
|
||||
5. 试着动手实践一些[例程](https://github.com/lvgl/lvgl/tree/master/examples)
|
||||
6. 参考[移植指南](https://docs.lvgl.io/master/porting/index.html)尝试将LVGL移植到一块开发板上,LVGL也已经提供了一些移植好的[工程](https://github.com/lvgl?q=lv_port_)
|
||||
7. 仔细阅读文档[总览](https://docs.lvgl.io/master/overview/index.html)章节来更加深入的了解和熟悉LVGL(2-3小时)
|
||||
8. 浏览文档[组件(Widgets)](https://docs.lvgl.io/master/widgets/index.html)章节来了解如何使用它们
|
||||
9. 如果你有问题可以到LVGL[论坛](http://forum.lvgl.io/)提问
|
||||
10. 阅读文档[如何向社区贡献](https://docs.lvgl.io/master/CONTRIBUTING.html)章节来看看你能帮LVGL社区做些什么,以促进LVGL软件质量的不断提高(15分钟)
|
||||
|
||||
## 例程
|
||||
|
||||
更多例程请参见 [examples](https://github.com/lvgl/lvgl/tree/master/examples) 文件夹。
|
||||
|
||||

|
||||
|
||||
### C
|
||||
```c
|
||||
lv_obj_t * btn = lv_btn_create(lv_scr_act()); /*Add a button to the current screen*/
|
||||
lv_obj_set_pos(btn, 10, 10); /*Set its position*/
|
||||
lv_obj_set_size(btn, 100, 50); /*Set its size*/
|
||||
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); /*Assign a callback to the button*/
|
||||
|
||||
lv_obj_t * label = lv_label_create(btn); /*Add a label to the button*/
|
||||
lv_label_set_text(label, "Button"); /*Set the labels text*/
|
||||
lv_obj_center(label); /*Align the label to the center*/
|
||||
...
|
||||
|
||||
void btn_event_cb(lv_event_t * e)
|
||||
{
|
||||
printf("Clicked\n");
|
||||
}
|
||||
```
|
||||
### Micropython
|
||||
更多信息请到 [Micropython官网](https://docs.lvgl.io/master/get-started/bindings/micropython.html) 查询.
|
||||
```python
|
||||
def btn_event_cb(e):
|
||||
print("Clicked")
|
||||
|
||||
# Create a Button and a Label
|
||||
btn = lv.btn(lv.scr_act())
|
||||
btn.set_pos(10, 10)
|
||||
btn.set_size(100, 50)
|
||||
btn.add_event_cb(btn_event_cb, lv.EVENT.CLICKED, None)
|
||||
|
||||
label = lv.label(btn)
|
||||
label.set_text("Button")
|
||||
label.center()
|
||||
```
|
||||
|
||||
## 服务
|
||||
LVGL 责任有限公司成立的目的是为了给用户使用LVGL图形库提供额外的技术支持,我们致力于提供以下服务:
|
||||
|
||||
- 图形设计
|
||||
- UI设计
|
||||
- 技术咨询以及技术支持
|
||||
|
||||
更多信息请参见 https://lvgl.io/services ,如果有任何问题请随时联系我们。
|
||||
|
||||
|
||||
## 如何向社区贡献
|
||||
LVGL是一个开源项目,非常欢迎您参与到社区贡献当中。您有很多种方式来为提高LVGL贡献您的一份力量,包括但不限于:
|
||||
|
||||
- 介绍你基于LVGL设计的作品或项目
|
||||
- 写一些例程
|
||||
- 修改以及完善文档
|
||||
- 修复bug
|
||||
|
||||
请参见文档[如何向社区贡献](https://docs.lvgl.io/master/CONTRIBUTING.html)章节来获取更多信息。
|
||||
@@ -0,0 +1,11 @@
|
||||
# RT-Thread building script for bridge
|
||||
|
||||
import os
|
||||
from building import *
|
||||
|
||||
objs = []
|
||||
cwd = GetCurrentDir()
|
||||
|
||||
objs = objs + SConscript(cwd + '/env_support/rt-thread/SConscript')
|
||||
|
||||
Return('objs')
|
||||
@@ -0,0 +1,34 @@
|
||||
# ESP-IDF component file for make based commands
|
||||
|
||||
COMPONENT_SRCDIRS := . \
|
||||
src \
|
||||
src/core \
|
||||
src/draw \
|
||||
src/extra \
|
||||
src/font \
|
||||
src/gpu \
|
||||
src/hal \
|
||||
src/misc \
|
||||
src/widgets \
|
||||
src/extra/layouts \
|
||||
src/extra/layouts/flex \
|
||||
src/extra/layouts/grid \
|
||||
src/extra/themes \
|
||||
src/extra/themes/basic \
|
||||
src/extra/themes/default \
|
||||
src/extra/widgets/calendar \
|
||||
src/extra/widgets/colorwheel \
|
||||
src/extra/widgets \
|
||||
src/extra/widgets/imgbtn \
|
||||
src/extra/widgets/keyboard \
|
||||
src/extra/widgets/led \
|
||||
src/extra/widgets/list \
|
||||
src/extra/widgets/msgbox \
|
||||
src/extra/widgets/spinbox \
|
||||
src/extra/widgets/spinner \
|
||||
src/extra/widgets/tabview \
|
||||
src/extra/widgets/tileview \
|
||||
src/extra/widgets/win
|
||||
|
||||
|
||||
COMPONENT_ADD_INCLUDEDIRS := $(COMPONENT_SRCDIRS) .
|
||||
@@ -0,0 +1,97 @@
|
||||
# Demos for LVGL
|
||||
|
||||
## Add the examples to your projects
|
||||
1. demos can be found in the 'demos' folder once you clone the lvgl.
|
||||
|
||||
2. In the ***lv_conf.h*** or equivalent places, you can find demo related macros, change its value to enable or disable specified demos:
|
||||
|
||||
```c
|
||||
...
|
||||
/*===================
|
||||
* DEMO USAGE
|
||||
====================*/
|
||||
|
||||
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
|
||||
#define LV_USE_DEMO_WIDGETS 0
|
||||
#if LV_USE_DEMO_WIDGETS
|
||||
#define LV_DEMO_WIDGETS_SLIDESHOW 0
|
||||
#endif
|
||||
|
||||
/*Demonstrate the usage of encoder and keyboard*/
|
||||
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
|
||||
|
||||
/*Benchmark your system*/
|
||||
#define LV_USE_DEMO_BENCHMARK 0
|
||||
|
||||
/*Stress test for LVGL*/
|
||||
#define LV_USE_DEMO_STRESS 0
|
||||
|
||||
/*Music player demo*/
|
||||
#define LV_USE_DEMO_MUSIC 0
|
||||
#if LV_USE_DEMO_MUSIC
|
||||
# define LV_DEMO_MUSIC_SQUARE 0
|
||||
# define LV_DEMO_MUSIC_LANDSCAPE 0
|
||||
# define LV_DEMO_MUSIC_ROUND 0
|
||||
# define LV_DEMO_MUSIC_LARGE 0
|
||||
# define LV_DEMO_MUSIC_AUTO_PLAY 0
|
||||
#endif
|
||||
...
|
||||
```
|
||||
|
||||
3. If your development environment or toolchain does not add source files inside '***lvgl***' folder automatically, ensure the `demos` folder is included for compilation.
|
||||
4. Include "***demos/lv_demos.h***" in your application source file, for example:
|
||||
|
||||
```c
|
||||
//! main.c
|
||||
#include "lvgl.h"
|
||||
#include "demos/lv_demos.h"
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Demos
|
||||
|
||||
### Widgets
|
||||
Shows how the widgets look like out of the box using the built-in material theme.
|
||||
|
||||
See in [widgets](https://github.com/lvgl/lvgl/tree/master/demos/widgets) folder.
|
||||
|
||||
<img src="https://github.com/lvgl/lvgl/tree/master/demos/widgets/screenshot1.png?raw=true" width=600px alt="Basic demo to show the widgets of LVGL">
|
||||
|
||||
For running this demo properly, please make sure **LV_MEM_SIZE** is at least **38KB** (and **48KB** is recommended):
|
||||
|
||||
```c
|
||||
#define LV_MEME_SIZE (38ul * 1024ul)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Music player
|
||||
The music player demo shows what kind of modern, smartphone-like user interfaces can be created on LVGL. It works the best with display with 480x272 or 272x480 resolution.
|
||||
|
||||
See in [music](https://github.com/lvgl/lvgl/tree/master/demos/music) folder.
|
||||
|
||||
<img src="https://github.com/lvgl/lvgl/tree/master/demos/music/screenshot1.gif?raw=true" width=600px alt="Music player demo with LVGL">
|
||||
|
||||
### Keypad and encoder
|
||||
LVGL allows you to control the widgets with a keypad and/or encoder without a touchpad. This demo shows how to handle buttons, drop-down lists, rollers, sliders, switches, and text inputs without touchpad.
|
||||
Learn more about the touchpad-less usage of LVGL [here](https://docs.lvgl.io/master/overview/indev.html#keypad-and-encoder).
|
||||
|
||||
See in [keypad_encoder](https://github.com/lvgl/lvgl/tree/master/demos/keypad_encoder) folder.
|
||||
|
||||
<img src="https://github.com/lvgl/lvgl/tree/master/demos/keypad_encoder/screenshot1.png?raw=true" width=600px alt="Keypad and encoder navigation in LVGL embedded GUI library">
|
||||
|
||||
### Benchmark
|
||||
A demo to measure the performance of LVGL or to compare different settings.
|
||||
See in [benchmark](https://github.com/lvgl/lvgl/tree/master/demos/benchmark) folder.
|
||||
<img src="https://github.com/lvgl/lvgl/tree/master/demos/benchmark/screenshot1.png?raw=true" width=600px alt="Benchmark demo with LVGL embedded GUI library">
|
||||
|
||||
### Stress
|
||||
A stress test for LVGL. It contains a lot of object creation, deletion, animations, style usage, and so on. It can be used if there is any memory corruption during heavy usage or any memory leaks.
|
||||
See in [stress](https://github.com/lvgl/lvgl/tree/master/demos/stress) folder.
|
||||
<img src="https://github.com/lvgl/lvgl/tree/master/demos/stress/screenshot1.png?raw=true" width=600px alt="Stress test for LVGL">
|
||||
|
||||
## Contributing
|
||||
For contribution and coding style guidelines, please refer to the file docs/CONTRIBUTING.md in the main LVGL repo:
|
||||
https://github.com/lvgl/lvgl
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user