Compare commits

...

39 Commits

Author SHA1 Message Date
Laan Tungir
64fbd5c874 v0.1.0 - CYD firmware v0.0.2: algorithm-based API upgrade (all verbs, all algorithms), vendored Keccak/SHAKE + PSA ed25519/x25519 for IDF v5.4, Web Serial test page, CYD docs, Teensy 4.1 port plan (1TB SDXC OTP pad), brainstorming READMEs for BLE/IR/NFC/FPGA signer concepts 2026-07-21 13:16:04 -04:00
Laan Tungir
ca18e1e42d v0.0.58 - Added derive verb: HMAC-SHA256(privkey, data) for secp256k1, enabling opaque d-tag derivation via nsigner remote backend without exposing the privkey 2026-07-20 19:56:51 -04:00
Laan Tungir
b3421c3e40 v0.0.57 - Migrated to unified nostr_ prefixed verb names; removed legacy verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate, kem_decapsulate, otp_encrypt, otp_decrypt); split get_public_key into algorithm-based get_public_key and role-based nostr_get_public_key; OTP now selected via algorithm:otp instead of curve:otp; consolidated API docs from api.md into README.md 2026-07-20 17:29:45 -04:00
Laan Tungir
96ab9741ef v0.0.56 - Fix connection display scroll issue: use full screen clear instead of tui_clear_continuous for modal connections view 2026-07-20 10:11:09 -04:00
Laan Tungir
2af12868e2 v0.0.55 - Remove redundant 'Press d for connection instructions' hint line from TUI (already in hotkey menu) 2026-07-20 09:55:10 -04:00
Laan Tungir
a0a5987ffa v0.0.54 - TUI: show full derivation path in Roles table, move connection instructions to on-demand 'd' hotkey display with spaced transport blocks 2026-07-20 09:49:20 -04:00
Laan Tungir
0b0ec5eb1a v0.0.53 - Fixed SIGILL crash in multi-listen mode: pfds array was too small (3) for 3 listeners + stdin (4), causing stack buffer overflow 2026-07-20 09:13:18 -04:00
Laan Tungir
0355744103 v0.0.52 - Added api.md with unified verb scheme, fixed TUI status display in README, added TCP/HTTP port auto-increment on EADDRINUSE (up to 5 tries) 2026-07-20 09:07:21 -04:00
Laan Tungir
db274ce487 v0.0.51 - Document memfd_secret as future secret-memory backing in README §2.5 (mlock remains current path; memfd_secret unusable on Qubes Xen guests due to SIGBUS on page materialization) 2026-07-19 14:35:00 -04:00
Laan Tungir
56f37e092d v0.0.50 - Clean up README: rename 4c.1 to 'Verbs' (no past-tense references), remove section 11 (Implemented adjuncts and future work) and section 12 (Document map) 2026-07-19 14:07:04 -04:00
Laan Tungir
a017dc40e0 v0.0.49 - Added general encrypt/decrypt verbs with curve-based routing (otp, secp256k1 NIP-04/44, x25519, ml-kem-768) and updated README documentation 2026-07-19 11:38:44 -04:00
Laan Tungir
05c055503d v0.0.48 - Added OTP one-time pad encryption (otp_encrypt/otp_decrypt verbs), HTTP listener mode (--listen http:HOST:PORT), interactive OTP pad auto-scan on USB drives, raised SERVER_MAX_MSG_SIZE to 16MB, updated README with curl examples and current API documentation 2026-07-19 11:07:07 -04:00
Laan Tungir
a7c6de2dcd v0.0.47 - Clean up main menu layout and hotkeys 2026-07-16 17:59:58 -04:00
Laan Tungir
16a6da817c Auto-fix Gitea release timestamp via SSH after release creation (workaround for Gitea created_unix=0 bug) 2026-07-16 16:30:15 -04:00
Laan Tungir
1cf541b02d Fix install script: use git tags API with version sorting instead of releases API (works around Gitea epoch-zero timestamp bug) 2026-07-16 16:13:11 -04:00
Laan Tungir
8015742e29 v0.0.46 - Post-quantum crypto expansion: ML-DSA-65, SLH-DSA-128s, ML-KEM-768, ed25519, x25519, algorithm-based API 2026-07-16 16:11:46 -04:00
Laan Tungir
09f3ec2f7c Fix Gitea release creation: include created_at timestamp to work around Gitea bug where releases get epoch zero timestamp 2026-07-16 16:10:00 -04:00
Laan Tungir
5744b83288 v0.0.47 - Fix increment_and_push.sh build output visibility 2026-07-16 15:38:52 -04:00
Laan Tungir
21892c108e Fix increment_and_push.sh hanging by showing build output instead of suppressing it 2026-07-16 15:37:10 -04:00
Laan Tungir
344add841c v0.0.48 - . 2026-07-16 15:35:20 -04:00
Laan Tungir
3e3013dde1 v0.0.47 - . 2026-07-16 15:33:25 -04:00
Laan Tungir
11d3760d7b v0.0.46 - Fix release tagging for post-quantum crypto expansion 2026-07-16 15:27:34 -04:00
Laan Tungir
c5f1a70658 v0.0.47 - Added post-quantum cryptography (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) and standard ECC (ed25519, x25519) support with algorithm-based API 2026-07-16 15:14:57 -04:00
Laan Tungir
6fd7b8ce1f v0.0.45 - Display qrexec service name (qubes.NsignerRpc) in signer connection info; use human-readable timestamps in activity log; fix static release build by adding miner.c to Dockerfile.alpine-musl 2026-07-11 19:12:34 -04:00
Laan Tungir
1b5af2fd33 v0.0.44 - Add JavaScript and Python demo programs (client/demo_javascript.js, client/demo_python.py) demonstrating get_public_key, sign_event, and nip44 encrypt/decrypt via qrexec 2026-07-11 15:01:48 -04:00
Laan Tungir
9b47883330 v0.0.43 - Add C99 demo program (client/demo_c99.c) demonstrating get_public_key, sign_event, and nip44 encrypt/decrypt via qrexec; sync nostr_core_lib with reconnect fix 2026-07-11 14:55:16 -04:00
Laan Tungir
9a8657f663 v0.0.42 - Add C qrexec client example using high-level nostr_signer API with nostr_index selector; sync nostr_core_lib with qrexec transport and index support 2026-07-11 14:28:09 -04:00
Laan Tungir
478c3a569e v0.0.41 - Fix activity log label for index whitelist denials — shows 'DENIED:index-not-approved' instead of 'DENIED:no-match' 2026-07-11 13:57:13 -04:00
Laan Tungir
10208e5fac v0.0.40 - Add interactive index whitelist prompt after transport selection — users can restrict nostr_index values without CLI flags 2026-07-11 13:46:23 -04:00
Laan Tungir
922a45ce3a v0.0.39 - Remove qrexec one-shot (option 4) from interactive transport menu — only persistent listeners remain 2026-07-11 13:42:59 -04:00
Laan Tungir
d28f691aae v0.0.38 - Add --allow-index whitelist for nostr_index restriction — supports list (1,3,4), range (0-3), mixed (0,2-3), or 'all' 2026-07-11 13:11:36 -04:00
Laan Tungir
2e8ce777d8 v0.0.37 - Add interactive multi-transport selection menu at startup — users can select one or more transports (unix, qrexec bridge, TCP) from a menu instead of memorizing CLI flags 2026-07-11 11:31:21 -04:00
Laan Tungir
8ebdb50789 v0.0.36 - Update README with qrexec bridge docs, update start_nsigner.sh to support qrexec mode and extra args, add setup scripts 2026-07-11 11:09:41 -04:00
Laan Tungir
a39baed82b v0.0.35 - Update README with qrexec bridge docs, update start_nsigner.sh to support qrexec mode and extra args 2026-07-11 11:08:04 -04:00
Laan Tungir
d15eebb80f v0.0.34 - Add qrexec bridge subcommand and --bridge-source-trusted flag for persistent-signer qrexec transport 2026-07-11 09:37:51 -04:00
Laan Tungir
fd622dfd60 server: ignore SIGPIPE so abrupt client disconnects don't kill the signer
The attended signer only handled SIGINT/SIGTERM. A client that disconnects
abruptly (e.g. nostr_core_lib's nsigner client reconnects per request and
closes the previous connection) could cause a write to the closed socket to
raise SIGPIPE, terminating the whole signer process after one request.

Ignore SIGPIPE process-wide so writes to closed sockets return EPIPE instead.
Verified end-to-end with nostr_core_lib note_poster --signer unix:nsigner-test
posting a kind-1 to relay.laantungir.net.
2026-07-08 09:46:42 -04:00
Laan Tungir
c847179ba4 v0.0.33 - Implement in-place mnemonic entry grid with staged prefix selection and fix stage-3 candidate rendering 2026-06-09 10:29:15 -04:00
Laan Tungir
69c46ab2a7 v0.0.32 - Fix KB2040 Python WebUSB handshake and robust frame reassembly for get-public-key 2026-06-08 20:24:02 -04:00
Laan Tungir
6fd09f521a v0.0.31 - Harden LVGL screen transitions with widget pointer resets and null guards 2026-05-28 09:22:42 -04:00
288 changed files with 106842 additions and 1835 deletions

3
.gitignore vendored
View File

@@ -6,4 +6,7 @@ build/
resources/ resources/
.test_mnemonic .test_mnemonic
Trash/ Trash/
node_modules/
package.json
package-lock.json

View 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"
}

View File

@@ -55,7 +55,9 @@ RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev
# Copy source files # Copy source files
COPY src/ /build/src/ COPY src/ /build/src/
COPY libotppad/ /build/libotppad/
COPY resources/tui_continuous/ /build/resources/tui_continuous/ COPY resources/tui_continuous/ /build/resources/tui_continuous/
COPY resources/pqclean/ /build/resources/pqclean/
# Build nsigner as a fully static binary # Build nsigner as a fully static binary
RUN ARCH="$(uname -m)"; \ RUN ARCH="$(uname -m)"; \
@@ -70,6 +72,12 @@ RUN ARCH="$(uname -m)"; \
-I/build/nostr_core_lib/nostr_core \ -I/build/nostr_core_lib/nostr_core \
-I/build/nostr_core_lib/cjson \ -I/build/nostr_core_lib/cjson \
-I/build/resources/tui_continuous \ -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/main.c \
/build/src/secure_mem.c \ /build/src/secure_mem.c \
/build/src/mnemonic.c \ /build/src/mnemonic.c \
@@ -83,6 +91,33 @@ RUN ARCH="$(uname -m)"; \
/build/src/key_store.c \ /build/src/key_store.c \
/build/src/socket_name.c \ /build/src/socket_name.c \
/build/src/auth_envelope.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 \ /build/resources/tui_continuous/tui_continuous.c \
"$NOSTR_LIB" \ "$NOSTR_LIB" \
-o /build/nsigner_static \ -o /build/nsigner_static \

158
Makefile
View File

@@ -1,5 +1,5 @@
CC := gcc 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 -Iresources/tui_continuous 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 LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
SRC_DIR := src SRC_DIR := src
@@ -10,6 +10,31 @@ EXAMPLES_DIR := examples
TARGET_DEV := $(BUILD_DIR)/nsigner 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 := \ SOURCES := \
$(SRC_DIR)/main.c \ $(SRC_DIR)/main.c \
$(SRC_DIR)/secure_mem.c \ $(SRC_DIR)/secure_mem.c \
@@ -24,6 +49,13 @@ SOURCES := \
$(SRC_DIR)/key_store.c \ $(SRC_DIR)/key_store.c \
$(SRC_DIR)/socket_name.c \ $(SRC_DIR)/socket_name.c \
$(SRC_DIR)/auth_envelope.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 resources/tui_continuous/tui_continuous.c
HEADERS := HEADERS :=
@@ -40,15 +72,29 @@ TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input 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_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_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 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 examples test-client 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 all: dev
lib: 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) dev: lib $(TARGET_DEV)
@@ -71,7 +117,7 @@ static-arm64:
firmware-feather: firmware-feather:
cd firmware/feather_s3_tft && idf.py build 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-client 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: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
./$(TEST_INTEGRATION_TARGET) ./$(TEST_INTEGRATION_TARGET)
@@ -106,13 +152,37 @@ test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV) test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
./$(TEST_QREXEC_AUTH_TARGET) ./$(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 test-client: examples
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) 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 $(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR) @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 $(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@@ -126,21 +196,21 @@ $(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC
@mkdir -p $(BUILD_DIR) @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) $(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) @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) @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 $(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c -o $(TEST_POLICY_TARGET) $(LDFLAGS) $(CC) $(CFLAGS) $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c -o $(TEST_POLICY_TARGET) $(LDFLAGS)
$(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c $(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(TEST_DIR)/test_integration.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS) $(CC) $(CFLAGS) $(TEST_DIR)/test_integration.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS)
$(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c $(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@@ -154,13 +224,69 @@ $(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envel
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS) $(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c $(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) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(EXAMPLES_DIR)/get_public_key_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS) $(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)
$(EXAMPLE_SIGN_EVENT_TARGET): $(EXAMPLES_DIR)/sign_event_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c $(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(EXAMPLES_DIR)/sign_event_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(EXAMPLE_SIGN_EVENT_TARGET) $(LDFLAGS) $(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: clean:
rm -rf $(BUILD_DIR) rm -rf $(BUILD_DIR)

714
README.md
View File

@@ -20,7 +20,8 @@ This is a **program, not a daemon**:
- purpose/curve enforcement - purpose/curve enforcement
- request dispatch - request dispatch
- interactive terminal UI - 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. 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 ### 2.1 Zero filesystem footprint
At runtime, `n_signer` writes nothing to disk: 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.
- 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 ### 2.2 Crash = total wipe
All sensitive and operational state exists only in-process RAM (mlock'd where applicable): 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.
- 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.
### 2.3 Single binary, no external dependencies ### 2.3 Single binary, no external dependencies
Runtime target is one statically-linked musl executable: 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.
- 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 ### 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. `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. How it works
### 3.1 Startup phase (TUI input mode) ### 3.1 Startup phase (TUI input mode)
@@ -74,9 +61,12 @@ When started, `n_signer` immediately enters terminal input mode:
- On `E`: prompt for mnemonic with terminal echo disabled, then validate. - 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. - 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. 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). 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. Initialize transport endpoints and bind the socket. 4. **Index whitelist** (optional): restrict which `nostr_index` values this session can access.
5. Switch to running status display, with the signer name and socket address shown in the banner. 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. 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.
@@ -89,48 +79,56 @@ These modes avoid putting mnemonic material in argv/environment and are designed
### 3.2 Running phase (status display + signer) ### 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 ```text
n_signer v0.x | foreground session active n_signer v0.0.53 > Main Menu
transport: unix-abstract:@nsigner
session: unlocked (RAM-only)
Roles Roles:
----- Role Purpose Curve Derivation path
main purpose=nostr curve=secp256k1 selector=role:main main nostr secp256k1 m/44'/1237'/0'/0/0
ops purpose=nostr curve=secp256k1 selector=nostr_index:7 nostr_idx_1 nostr secp256k1 m/44'/1237'/1'/0/0
backup purpose=bitcoin curve=secp256k1 selector=role_path:m/84'/0'/0'/0/5 backup bitcoin secp256k1 m/84'/0'/0'/0/5
Pending approvals Activity (latest first):
----------------- 16:03:11 allow caller=uid:1000 method=nostr_get_public_key role=main
(none) 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) session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
-----------------------
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
Hotkeys l lock/reunlock
------- r refresh
a toggle auto-approve(prompt) for this session a toggle auto-approve
r refresh d display connections
l lock/reunlock session q/x quit
q 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 ### 3.3 Approval prompts
When a request needs confirmation, `n_signer` interrupts the status view with a prompt and waits for a local keystroke. When a request needs confirmation, `n_signer` interrupts the status view with a prompt and waits for a local keystroke.
Example:
```text ```text
Approval required Approval required
caller: uid:1000 caller: uid:1000
method: sign_event method: nostr_sign_event
selector: role=ops selector: role=ops
purpose/curve: nostr/secp256k1 purpose/curve: nostr/secp256k1
@@ -145,123 +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. - `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. - 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` ```json
{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] }
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" }] }
``` ```
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` ### 4.2 Response format
- `sign_event`
- `nip04_encrypt` / `nip04_decrypt`
- `nip44_encrypt` / `nip44_decrypt`
Selector resolution order: Success:
```json
{ "id": "<string>", "result": <value> }
```
1. `role` `result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it.
2. `nostr_index`
3. `role_path`
4. default role `main`
Conflicting selectors are rejected (`ambiguous_role_selector`). Error:
```json
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
```
Representative error codes: Error codes:
- `invalid_request` | Code | Message | Meaning |
- `method_not_found` |-------|-------------------------------|--------------------------------------------------------------------|
- `ambiguous_role_selector` | -32700| `parse_error` | Request was not valid JSON. |
- `unknown_role` | -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. |
- `purpose_mismatch` | -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. |
- `curve_mismatch` | -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). |
- `unauthorized` | 1001 | `ambiguous_role_selector` | More than one role selector was supplied. |
- `approval_denied` | 1002 | `unknown_role` | No role matched the selector. |
- `internal_error` | 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"`. \* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
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 | Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) |
- endpoint lifetime bound to process/kernel namespace |-----------------|-----------------|---------------|---------------------------------------|-------------------------------|
- no stale socket files | `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 |
- caller identity via peer credentials (`SO_PEERCRED`) | `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
- per-launch random name avoids collisions between concurrent instances and leaks no seed-derived identifier | `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: Naming rules:
- Default: random pick at startup, displayed in the status line.
- Default: random pick at startup, displayed in the TUI banner. - Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name.
- 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, `nsigner` retries with a fresh pair up to 8 times before erroring out.
- 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.
Discovery: Discovery:
- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`. - `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 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 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 TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:8080`, `tcp:[::]:8080`, or `tcp:[fd00::1234]:8080`). - `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: TinyUSB composite (CDC + WebUSB) ### 5.4 ESP32 MCU: TinyUSB composite (CDC + WebUSB)
On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport: 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.
- CDC-ACM framed JSON-RPC for serial tooling (`/dev/ttyACM*`) ### 5.5 Caller verification
- Vendor/WebUSB framed JSON-RPC for browser tooling
Core signer 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
Every transport must provide concrete caller identity before policy evaluation. Every transport must provide concrete caller identity before policy evaluation.
@@ -271,105 +577,131 @@ 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. 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. 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. The Feather path currently uses TinyUSB composite USB (CDC + WebUSB) plus TFT/buttons for attended approvals. 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 ```bash
nsigner 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): To force a specific socket name (e.g. for scripted clients):
```bash ```bash
nsigner --name my_test_signer nsigner --name my_test_signer
``` ```
Qubes/qrexec service mode (single framed request over stdin/stdout): Other transport modes:
```bash ```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 ```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 ```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: From another terminal, target the signer by its socket name:
```bash ```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. If only one signer is running you can omit the override and the client will use the default discovery rule.
Example signing request: Example signing request:
```bash ```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 ```bash
nsigner list nsigner list
``` ```
Prints the abstract socket names of any currently running `nsigner` instances, e.g.: Prints the abstract socket names of any currently running `nsigner` instances, e.g.:
```text ```text
@nsigner_hairy_dog @nsigner_hairy_dog
@nsigner_brave_canyon @nsigner_brave_canyon
``` ```
### 9.4 Example session ### 7.4 Example session
Terminal A: Terminal A:
```text ```text
$ nsigner $ nsigner
[unlock] enter mnemonic: [unlock] enter mnemonic:
[ok] session unlocked System is ready and waiting for connections on @nsigner_hairy_dog.
signer name : hairy dog [prompt] caller=uid:1000 method=nostr_sign_event role=main -> allow? (y/n)
socket : @nsigner_hairy_dog
[listen] unix-abstract:@nsigner_hairy_dog
[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n)
``` ```
Terminal B: Terminal B:
```text ```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>"} {"id":"2","result":"<signed_event_json>"}
``` ```
## 10. Build and versioning ## 8. Build and versioning
Build outputs a single executable artifact. Build outputs a single executable artifact.
@@ -381,43 +713,13 @@ Build outputs a single executable artifact.
- [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`) - [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`)
Local dev build: Local dev build:
```bash ```bash
make dev make dev
./build/nsigner --version ./build/nsigner --version
``` ```
Static build: Static build:
```bash ```bash
./build_static.sh ./build_static.sh
./build/nsigner_static_x86_64 --version ./build/nsigner_static_x86_64 --version
``` ```
## 11. Implemented adjuncts and future work
### Implemented PoC
- **MCU / USB signer (Feather ESP32-S3 Reverse TFT).** Working PoC in [`firmware/feather_s3_tft`](firmware/feather_s3_tft). Single TinyUSB composite USB device exposes both CDC-ACM and WebUSB Vendor interfaces. Same dispatcher serves both transports with auth envelope verification, on-device TFT prompts, and physical button approval. See [`firmware/README.md`](firmware/README.md) and [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md).
### Future work (deferred)
These items are designed and worth doing, but are not in the current implementation scope. They are listed here so they are not lost.
- **`--listen http:[addr]:port` mode.** Today the TCP listener speaks 4-byte big-endian length-prefixed framed JSON-RPC, which is correct for low-overhead local IPC but is not directly reachable from web browsers (`fetch`, `XMLHttpRequest`, `curl`). A small additional listener that wraps the same dispatcher in minimal HTTP/1.1 (`POST /rpc`, `Content-Type: application/json`, `Content-Length`-framed body, JSON response) would let standard HTTP clients talk to `nsigner` without any custom framing code. The existing auth envelope (`kind:27235`) and JSON-RPC contract are unchanged; only the outer framing differs. CORS allow on the response would let browser extensions and (with TLS) HTTPS pages reach a remote `nsigner` over FIPS or any other carrier. See the discussion in [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md) section 9 ("Next hardening steps").
- **NIP-46 bunker / relay transport.** Tracked in [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md).
- **Browser extension.** Tracked in [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md). NIP-07 surface forwarding to a running `nsigner` instance.
## 12. 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/feather_s3_tft`](firmware/feather_s3_tft): Feather ESP32-S3 Reverse TFT firmware (TinyUSB composite CDC + WebUSB signer PoC)
- [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md): firmware Phase 7b plan and outcome (TinyUSB composite USB transport)
- [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md): browser extension exposing NIP-07 over `nsigner`
- [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md): deferred NIP-46 relay-mode signer transport
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration

15
api.md Normal file
View 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).

View File

@@ -1,47 +1,113 @@
# n_signer C Reference Client # n_signer C Client — migrated to `nostr_core_lib`
This directory provides a minimal copy/paste-friendly C client for `n_signer`. 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):
## Files - [`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).
- `nsigner_client.h` / `nsigner_client.c`: This is the single source of truth for the n_signer wire contract. See
- Unix abstract socket connect helper [`nostr_core/NSIGNER_INTEGRATION.md`](../resources/nostr_core_lib/nostr_core/NSIGNER_INTEGRATION.md)
- length-prefixed frame send/receive for the full integration contract.
- generic request API
- helpers for `get_public_key` and `sign_event`
- optional TCP auth envelope attachment via `auth_envelope_build_for_request()`
## API at a glance ## What moved where
```c | Old (`client/`) | New (`nostr_core_lib`) |
nsigner_client_t client; |---|---|
nsigner_client_init(&client); | `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) |
nsigner_client_connect_unix(&client, "nsigner", 5000); ## Consumers (updated)
char *resp = NULL; - [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) — uses `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call`.
nsigner_client_get_public_key(&client, "1", "", &resp); - [`examples/sign_event_client.c`](../examples/sign_event_client.c) — same pattern.
free(resp); - [`tests/test_integration.c`](../tests/test_integration.c) — same pattern; cJSON-based assertions.
nsigner_client_close(&client); 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.
## Auth envelope usage ## Multi-Algorithm and Post-Quantum Verbs
If transport requires auth envelopes (for TCP mode), set a private key once: 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).
```c The API has two verb families (see [`README.md`](../README.md#4-api) §4 for the full spec):
unsigned char privkey[32] = { /* caller key */ };
nsigner_client_set_auth(&client, privkey, "example-client");
```
Subsequent requests automatically include an `auth` object. **Algorithm-based verbs** — the caller specifies `algorithm` and `index` in the
options object. No role table entry is needed.
## Ownership rules | 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"`) |
- Any `out_response_json` returned by the API must be freed by caller using `free()`. **Nostr protocol verbs** — select a secp256k1 NIP-06 key via `nostr_index` (or
- `nsigner_client_close()` only closes the socket; it does not free the client struct itself. `role`/`role_path`). These are role-based.
## License | 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 |
Source files in this directory use SPDX `0BSD` headers for permissive reuse in external projects. 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
View 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
View 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
View 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()

View File

@@ -1,369 +0,0 @@
/*
* SPDX-License-Identifier: 0BSD
*/
#define _GNU_SOURCE
#include "nsigner_client.h"
#include <arpa/inet.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#include <cJSON.h>
#include "auth_envelope.h"
#define NSIGNER_CLIENT_MAX_FRAME (65536U)
static void client_set_error(nsigner_client_t *client, const char *fmt, ...) {
va_list ap;
if (client == NULL) {
return;
}
va_start(ap, fmt);
vsnprintf(client->last_error, sizeof(client->last_error), fmt, ap);
va_end(ap);
}
static int write_full(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
static int read_full(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, p + off, len - off);
if (n == 0) {
return -1;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
static int send_framed(int fd, const char *payload) {
uint32_t len;
uint32_t be_len;
if (payload == NULL) {
return -1;
}
len = (uint32_t)strlen(payload);
be_len = htonl(len);
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
if (write_full(fd, payload, len) != 0) {
return -1;
}
return 0;
}
static int recv_framed(int fd, char **out_payload) {
uint32_t be_len;
uint32_t len;
char *payload;
if (out_payload == NULL) {
return -1;
}
*out_payload = NULL;
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
len = ntohl(be_len);
if (len == 0 || len > NSIGNER_CLIENT_MAX_FRAME) {
return -1;
}
payload = (char *)malloc((size_t)len + 1U);
if (payload == NULL) {
return -1;
}
if (read_full(fd, payload, len) != 0) {
free(payload);
return -1;
}
payload[len] = '\0';
*out_payload = payload;
return 0;
}
static int sleep_ms(int ms) {
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
return nanosleep(&ts, NULL);
}
void nsigner_client_init(nsigner_client_t *client) {
if (client == NULL) {
return;
}
memset(client, 0, sizeof(*client));
client->fd = -1;
client->timeout_ms = 5000;
}
void nsigner_client_close(nsigner_client_t *client) {
if (client == NULL) {
return;
}
if (client->fd >= 0) {
close(client->fd);
client->fd = -1;
}
}
int nsigner_client_connect_unix(nsigner_client_t *client,
const char *socket_name,
int timeout_ms) {
struct sockaddr_un addr;
socklen_t addr_len;
int elapsed = 0;
if (client == NULL || socket_name == NULL || socket_name[0] == '\0') {
return -1;
}
timeout_ms = (timeout_ms > 0) ? timeout_ms : 5000;
nsigner_client_close(client);
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
strncpy(&addr.sun_path[1], socket_name, sizeof(addr.sun_path) - 2);
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(socket_name));
while (elapsed < timeout_ms) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
client_set_error(client, "socket failed: %s", strerror(errno));
return -1;
}
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
client->fd = fd;
client->timeout_ms = timeout_ms;
client_set_error(client, "ok");
return 0;
}
close(fd);
sleep_ms(100);
elapsed += 100;
}
client_set_error(client, "connect timeout: %s", strerror(errno));
return -1;
}
int nsigner_client_set_auth(nsigner_client_t *client,
const unsigned char privkey[32],
const char *label) {
if (client == NULL || privkey == NULL) {
return -1;
}
memcpy(client->auth_privkey, privkey, 32);
client->auth_enabled = 1;
if (label == NULL) {
label = "";
}
strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
client->auth_label[sizeof(client->auth_label) - 1] = '\0';
return 0;
}
const char *nsigner_client_last_error(const nsigner_client_t *client) {
if (client == NULL) {
return "invalid_client";
}
return client->last_error;
}
int nsigner_client_request_raw(nsigner_client_t *client,
const char *request_json,
char **out_response_json) {
char *response_json = NULL;
if (client == NULL || request_json == NULL || out_response_json == NULL) {
return -1;
}
if (client->fd < 0) {
client_set_error(client, "not connected");
return -1;
}
*out_response_json = NULL;
if (send_framed(client->fd, request_json) != 0) {
client_set_error(client, "send failed");
return -1;
}
if (recv_framed(client->fd, &response_json) != 0) {
client_set_error(client, "receive failed");
return -1;
}
*out_response_json = response_json;
client_set_error(client, "ok");
return 0;
}
int nsigner_client_request(nsigner_client_t *client,
const char *id,
const char *method,
const cJSON *params,
char **out_response_json) {
cJSON *root = NULL;
cJSON *params_copy = NULL;
cJSON *auth = NULL;
char *request_json = NULL;
int rc = -1;
if (client == NULL || id == NULL || method == NULL || out_response_json == NULL) {
return -1;
}
root = cJSON_CreateObject();
if (root == NULL) {
client_set_error(client, "out of memory");
goto cleanup;
}
cJSON_AddStringToObject(root, "id", id);
cJSON_AddStringToObject(root, "method", method);
if (params != NULL) {
params_copy = cJSON_Duplicate((cJSON *)params, 1);
} else {
params_copy = cJSON_CreateArray();
}
if (params_copy == NULL) {
client_set_error(client, "failed to create params");
goto cleanup;
}
cJSON_AddItemToObject(root, "params", params_copy);
if (client->auth_enabled) {
if (auth_envelope_build_for_request(id,
method,
params_copy,
client->auth_privkey,
client->auth_label,
time(NULL),
&auth) != 0) {
client_set_error(client, "failed to build auth envelope");
goto cleanup;
}
cJSON_AddItemToObject(root, "auth", auth);
auth = NULL;
}
request_json = cJSON_PrintUnformatted(root);
if (request_json == NULL) {
client_set_error(client, "failed to serialize request");
goto cleanup;
}
rc = nsigner_client_request_raw(client, request_json, out_response_json);
cleanup:
cJSON_Delete(root);
cJSON_Delete(auth);
free(request_json);
return rc;
}
int nsigner_client_get_public_key(nsigner_client_t *client,
const char *id,
const char *selector,
char **out_response_json) {
cJSON *params = cJSON_CreateArray();
int rc;
if (params == NULL) {
return -1;
}
cJSON_AddItemToArray(params, cJSON_CreateString((selector != NULL) ? selector : ""));
rc = nsigner_client_request(client, id, "get_public_key", params, out_response_json);
cJSON_Delete(params);
return rc;
}
int nsigner_client_sign_event(nsigner_client_t *client,
const char *id,
const char *event_json,
const char *role,
char **out_response_json) {
cJSON *params = cJSON_CreateArray();
cJSON *opts = cJSON_CreateObject();
int rc;
if (params == NULL || opts == NULL || event_json == NULL) {
cJSON_Delete(params);
cJSON_Delete(opts);
return -1;
}
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
if (role != NULL && role[0] != '\0') {
cJSON_AddStringToObject(opts, "role", role);
}
cJSON_AddItemToArray(params, opts);
rc = nsigner_client_request(client, id, "sign_event", params, out_response_json);
cJSON_Delete(params);
return rc;
}

View File

@@ -1,56 +0,0 @@
/*
* SPDX-License-Identifier: 0BSD
*/
#ifndef NSIGNER_CLIENT_H
#define NSIGNER_CLIENT_H
#include <stddef.h>
#include <stdint.h>
#include <cJSON.h>
typedef struct {
int fd;
int timeout_ms;
unsigned char auth_privkey[32];
int auth_enabled;
char auth_label[64];
char last_error[128];
} nsigner_client_t;
void nsigner_client_init(nsigner_client_t *client);
void nsigner_client_close(nsigner_client_t *client);
int nsigner_client_connect_unix(nsigner_client_t *client,
const char *socket_name,
int timeout_ms);
int nsigner_client_set_auth(nsigner_client_t *client,
const unsigned char privkey[32],
const char *label);
const char *nsigner_client_last_error(const nsigner_client_t *client);
int nsigner_client_request_raw(nsigner_client_t *client,
const char *request_json,
char **out_response_json);
int nsigner_client_request(nsigner_client_t *client,
const char *id,
const char *method,
const cJSON *params,
char **out_response_json);
int nsigner_client_get_public_key(nsigner_client_t *client,
const char *id,
const char *selector,
char **out_response_json);
int nsigner_client_sign_event(nsigner_client_t *client,
const char *id,
const char *event_json,
const char *role,
char **out_response_json);
#endif

View File

@@ -127,6 +127,26 @@ Methods are NIP-46 style verbs.
- `nip44_encrypt` - `nip44_encrypt`
- `nip44_decrypt` - `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 ### 4.3 Selector options
The last param may include selector options: The last param may include selector options:
@@ -484,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. - 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). - Keep method support feature-detected (`method_not_found` fallback).

View File

@@ -61,13 +61,13 @@ Operational assumptions:
Run `nsigner` in TCP listen mode: Run `nsigner` in TCP listen mode:
```bash ```bash
./build/nsigner --listen tcp:[::]:8080 ./build/nsigner --listen tcp:[::]:11111
``` ```
Or bind to a specific FIPS ULA address: Or bind to a specific FIPS ULA address:
```bash ```bash
./build/nsigner --listen tcp:[fd00::1234]:8080 ./build/nsigner --listen tcp:[fd00::1234]:11111
``` ```
Behavior notes: Behavior notes:

View File

@@ -160,6 +160,58 @@ Includes:
- missing policy -> deny path - missing policy -> deny path
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity 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 ## 6. Security requirements

View File

@@ -215,12 +215,18 @@ The signer enforces a strict `(verb, purpose, curve)` matrix:
| Verb | Required purpose | Required curve | | Verb | Required purpose | Required curve |
|---|---|---| |---|---|---|
| `sign_event` | `nostr` | `secp256k1` | | `sign_event` | `nostr` | `secp256k1` |
| `get_public_key` | `nostr` | `secp256k1` | | `mine_event` | `nostr` | `secp256k1` |
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` | | `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
| `nip44_encrypt` / `nip44_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 | | 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 are rejected, never passed through. 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. 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.
@@ -508,7 +514,86 @@ If any of these statements becomes false in code, that is a security bug worth f
--- ---
## 16. References ---
## 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 **530 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. - [`README.md`](../README.md) — authoritative behavior spec.
- [`plans/nsigner.md`](../plans/nsigner.md) — root design plan and decisions log. - [`plans/nsigner.md`](../plans/nsigner.md) — root design plan and decisions log.
@@ -517,4 +602,5 @@ If any of these statements becomes false in code, that is a security bug worth f
- [`documents/QUBES_OS.md`](QUBES_OS.md) — Qubes RPC integration. - [`documents/QUBES_OS.md`](QUBES_OS.md) — Qubes RPC integration.
- [`documents/FIPS_DEPLOYMENT.md`](FIPS_DEPLOYMENT.md) — FIPS-mode deployment notes. - [`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/seed_phrase_uses.md`](../plans/seed_phrase_uses.md) — what one mnemonic can become.
- [`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) — the security-related code. - [`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.

View 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>

View File

@@ -140,11 +140,11 @@ def main() -> int:
req = { req = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": "2", "id": "2",
"method": "sign_event", "method": "nostr_sign_event",
"params": params, "params": params,
} }
if not no_auth: if not no_auth:
req["auth"] = build_auth_envelope("sign_event", params, caller_priv) req["auth"] = build_auth_envelope("nostr_sign_event", params, caller_priv)
body = json.dumps(req, separators=(",", ":")).encode("utf-8") body = json.dumps(req, separators=(",", ":")).encode("utf-8")
frame = struct.pack(">I", len(body)) + body frame = struct.pack(">I", len(body)) + body

View File

@@ -460,7 +460,7 @@
}; };
const params = [unsignedEvent, getIndexOptions()]; const params = [unsignedEvent, getIndexOptions()];
const resp = await rpcCall("sign_event", params, "web-sign-kind1"); const resp = await rpcCall("nostr_sign_event", params, "web-sign-kind1");
signOutEl.textContent = pretty(resp?.result ?? resp); signOutEl.textContent = pretty(resp?.result ?? resp);
} catch (e) { } catch (e) {
signOutEl.textContent = String(e); signOutEl.textContent = String(e);
@@ -472,7 +472,7 @@
const peer = requirePeerHex(nip04PeerEl.value); const peer = requirePeerHex(nip04PeerEl.value);
const msg = String(nip04MsgEl.value || ""); const msg = String(nip04MsgEl.value || "");
const params = [peer, msg, getIndexOptions()]; const params = [peer, msg, getIndexOptions()];
const resp = await rpcCall("nip04_encrypt", params, "web-nip04-enc"); const resp = await rpcCall("nostr_nip04_encrypt", params, "web-nip04-enc");
nip04OutEl.textContent = pretty(resp?.result ?? resp); nip04OutEl.textContent = pretty(resp?.result ?? resp);
if (resp && typeof resp.result === "string") { if (resp && typeof resp.result === "string") {
nip04DecPeerEl.value = peer; nip04DecPeerEl.value = peer;
@@ -488,7 +488,7 @@
const peer = requirePeerHex(nip04DecPeerEl.value); const peer = requirePeerHex(nip04DecPeerEl.value);
const ciphertext = String(nip04CipherEl.value || ""); const ciphertext = String(nip04CipherEl.value || "");
const params = [peer, ciphertext, getIndexOptions()]; const params = [peer, ciphertext, getIndexOptions()];
const resp = await rpcCall("nip04_decrypt", params, "web-nip04-dec"); const resp = await rpcCall("nostr_nip04_decrypt", params, "web-nip04-dec");
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt"); nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
} catch (e) { } catch (e) {
nip04DecOutEl.textContent = String(e); nip04DecOutEl.textContent = String(e);
@@ -500,7 +500,7 @@
const peer = requirePeerHex(nip44PeerEl.value); const peer = requirePeerHex(nip44PeerEl.value);
const msg = String(nip44MsgEl.value || ""); const msg = String(nip44MsgEl.value || "");
const params = [peer, msg, getIndexOptions()]; const params = [peer, msg, getIndexOptions()];
const resp = await rpcCall("nip44_encrypt", params, "web-nip44-enc"); const resp = await rpcCall("nostr_nip44_encrypt", params, "web-nip44-enc");
nip44OutEl.textContent = pretty(resp?.result ?? resp); nip44OutEl.textContent = pretty(resp?.result ?? resp);
if (resp && typeof resp.result === "string") { if (resp && typeof resp.result === "string") {
nip44DecPeerEl.value = peer; nip44DecPeerEl.value = peer;
@@ -516,7 +516,7 @@
const peer = requirePeerHex(nip44DecPeerEl.value); const peer = requirePeerHex(nip44DecPeerEl.value);
const ciphertext = String(nip44CipherEl.value || ""); const ciphertext = String(nip44CipherEl.value || "");
const params = [peer, ciphertext, getIndexOptions()]; const params = [peer, ciphertext, getIndexOptions()];
const resp = await rpcCall("nip44_decrypt", params, "web-nip44-dec"); const resp = await rpcCall("nostr_nip44_decrypt", params, "web-nip44-dec");
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt"); nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
} catch (e) { } catch (e) {
nip44DecOutEl.textContent = String(e); nip44DecOutEl.textContent = String(e);

View File

@@ -8,7 +8,7 @@ import time
from coincurve import PrivateKey from coincurve import PrivateKey
HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips" HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips"
PORT = 8080 PORT = 11111
# Demo caller key (32 bytes). Replace with your stable caller key in real use. # Demo caller key (32 bytes). Replace with your stable caller key in real use.
PRIVKEY = bytes(range(1, 33)) PRIVKEY = bytes(range(1, 33))

View 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
View 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;
}

View File

@@ -1,32 +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 <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "../client/nsigner_client.h" #include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "../cjson/cJSON.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
const char *socket_name = "nsigner"; const char *socket_name = "nsigner";
nsigner_client_t client; nsigner_transport_t *transport = NULL;
char *response = 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') { if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
socket_name = argv[1]; socket_name = argv[1];
} }
nsigner_client_init(&client); if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "failed to initialize crypto subsystem\n");
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
return 1; return 1;
} }
if (nsigner_client_get_public_key(&client, "example-1", "", &response) != 0) { transport = nsigner_transport_open_unix(socket_name, 5000);
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client)); if (transport == NULL) {
nsigner_client_close(&client); fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
return 1; goto cleanup;
} }
printf("%s\n", response); client = nsigner_client_new(transport);
free(response); if (client == NULL) {
nsigner_client_close(&client); fprintf(stderr, "connect failed: cannot create nsigner client\n");
return 0; /* 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;
} }

View 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())

View 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);
});

View 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
View 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
View 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
View 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;
}

View File

@@ -1,33 +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 <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "../client/nsigner_client.h" #include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "../cjson/cJSON.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
const char *socket_name = "nsigner"; const char *socket_name = "nsigner";
const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}"; const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}";
nsigner_client_t client; const char *role = "main";
char *response = NULL; 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') { if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
socket_name = argv[1]; socket_name = argv[1];
} }
nsigner_client_init(&client); if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "failed to initialize crypto subsystem\n");
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
return 1; return 1;
} }
if (nsigner_client_sign_event(&client, "example-2", event_json, "main", &response) != 0) { transport = nsigner_transport_open_unix(socket_name, 5000);
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client)); if (transport == NULL) {
nsigner_client_close(&client); fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
return 1; goto cleanup;
} }
printf("%s\n", response); client = nsigner_client_new(transport);
free(response); if (client == NULL) {
nsigner_client_close(&client); fprintf(stderr, "connect failed: cannot create nsigner client\n");
return 0; 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
View 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;
}

View File

@@ -53,6 +53,26 @@ WebUSB path:
- Run `get_public_key` - Run `get_public_key`
- Confirm pubkey matches CDC result - 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) ## 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: Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules:
@@ -99,3 +119,166 @@ Notes:
- Typical working range is ~4.7uF to 22uF; 10uF is recommended - Typical working range is ~4.7uF to 22uF; 10uF is recommended
- Keep leads short for best stability - Keep leads short for best stability
- Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually - 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**: 530 seconds
- **Signing**: 530 seconds
- **Verification**: 0.52 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, ...);
```

View 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))

View 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 432 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 530 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.722 µ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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View 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.

View 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)

View 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|

View 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!

View 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.
![image](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/assets/1562562/76c3d481-2523-4b6f-881c-2e29f9368cd0)
## 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?
![CYD decision tree](http://www.plantuml.com/plantuml/png/RP0nJyCm48Nt_8gZNIb3fge3LD2b2q92235UamDRE7PaNuhyxxda7DGgJBs-zxtSE-yJO-IXSzKD6-e8UeVMLyQs1DJrdA6br4JRims-4fW9LiS4bY6JS-47qBTWC052QvEayyCAvA-wS-8vi01F7mS8SVevOxJeUK9zu55QzzP_Nw-exxPmz8tHJzRRsJq4cdo3Pu98oIQsCd4O6WDIbyXF4LN-JNMsYG7UNXyXUAUTLHDfqVeMJWClUfSPrY_OOyPtO_ivUPcfnoMV3iyXJh4cj_MGJd8lEleQkvQKi9TYUT_DvbukXnraIfTQURMT39Nu8kcrXInIwQYO-gCyNwgm6al-ZneTNIRqjLokqS2UV3jqxXS0)
## 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/)

View 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/)

View 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.

View 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.
![image](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/assets/1562562/d23bf84f-f34b-4814-b609-87c359d6334e)
## 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 doesnt 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);
```

View File

@@ -10,6 +10,7 @@ idf_component_register(
"bech32.c" "bech32.c"
"mnemonic.c" "mnemonic.c"
"key_derivation.c" "key_derivation.c"
"pq_crypto_firmware.c"
"secure_mem.c" "secure_mem.c"
"../../../resources/nostr_core_lib/nostr_core/nip004.c" "../../../resources/nostr_core_lib/nostr_core/nip004.c"
"../../../resources/nostr_core_lib/nostr_core/nip044.c" "../../../resources/nostr_core_lib/nostr_core/nip044.c"
@@ -31,6 +32,7 @@ idf_component_register(
esp_driver_uart esp_driver_uart
mbedtls mbedtls
secp256k1 secp256k1
pqclean
json) json)
target_compile_definitions(${COMPONENT_LIB} PUBLIC LV_CONF_INCLUDE_SIMPLE=1) target_compile_definitions(${COMPONENT_LIB} PUBLIC LV_CONF_INCLUDE_SIMPLE=1)

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h" #include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "esp_random.h" #include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h" #include "mbedtls/md.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "psa/crypto.h"
#include "secp256k1.h" #include "secp256k1.h"
#include "secp256k1_extrakeys.h" #include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h" #include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u #define BIP32_HARDENED_FLAG 0x80000000u
typedef struct { typedef struct {
@@ -253,3 +261,351 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx); secp256k1_context_destroy(ctx);
return 0; 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;
}

View File

@@ -1,7 +1,70 @@
#pragma once #pragma once
#include <stddef.h>
#include <stdint.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(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 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]); 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);

File diff suppressed because it is too large Load Diff

View File

@@ -277,6 +277,72 @@ size_t mnemonic_words_for_letter(char letter, size_t *out_start)
return s_letter_count[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) const char *mnemonic_word_at(size_t index)
{ {
if (index >= 2048) { if (index >= 2048) {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -7,4 +8,6 @@ int generate_mnemonic_12(char *out, size_t out_len);
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]); int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]);
int mnemonic_validate(const char *mnemonic); int mnemonic_validate(const char *mnemonic);
size_t mnemonic_words_for_letter(char letter, size_t *out_start); 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); const char *mnemonic_word_at(size_t index);

View 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);
}

View 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 */

View File

@@ -13,10 +13,15 @@
#include "ili9341.h" #include "ili9341.h"
#include "lvgl.h" #include "lvgl.h"
#include "mnemonic.h" #include "mnemonic.h"
#include "secure_mem.h"
#include "touch.h" #include "touch.h"
#define UI_TICK_MS 2 #define UI_TICK_MS 2
#define UI_DRAW_LINES 20 #define UI_DRAW_LINES 20
#define UI_MNEMONIC_WORDS 12
#define UI_BIP39_WORD_MAX 9
#define UI_SUGGESTION_STORE_MAX 64
#define UI_SUGGESTION_VISIBLE_MAX 32
static const char *TAG = "ui_lvgl"; static const char *TAG = "ui_lvgl";
@@ -30,7 +35,21 @@ static bool s_done = false;
static bool s_have_mnemonic = false; static bool s_have_mnemonic = false;
static char s_selected_mnemonic[256]; static char s_selected_mnemonic[256];
static lv_obj_t *s_status_label = NULL; static lv_obj_t *s_status_label = NULL;
static lv_obj_t *s_entry_ta = NULL;
static char s_committed_words[UI_MNEMONIC_WORDS][UI_BIP39_WORD_MAX];
static size_t s_word_count = 0;
static char s_prefix[UI_BIP39_WORD_MAX];
static size_t s_prefix_len = 0;
static bool s_checksum_override = false;
static lv_obj_t *s_entry_words_label = NULL;
static lv_obj_t *s_entry_prefix_label = NULL;
static lv_obj_t *s_suggestion_row = NULL;
static lv_obj_t *s_key_matrix = NULL;
static lv_obj_t *s_checksum_panel = NULL;
static uint16_t s_match_indices[UI_SUGGESTION_STORE_MAX];
static size_t s_match_total = 0;
static ui_approval_decision_t s_approval_decision = UI_APPROVAL_TIMEOUT; static ui_approval_decision_t s_approval_decision = UI_APPROVAL_TIMEOUT;
static lv_obj_t *s_approval_countdown = NULL; static lv_obj_t *s_approval_countdown = NULL;
@@ -41,6 +60,18 @@ static int64_t s_approval_deadline_us = 0;
static char s_event_lines[UI_EVENT_LOG_MAX][UI_EVENT_LINE_MAX]; static char s_event_lines[UI_EVENT_LOG_MAX][UI_EVENT_LINE_MAX];
static int s_event_count = 0; static int s_event_count = 0;
static void reset_screen_widget_refs(void)
{
s_status_label = NULL;
s_entry_words_label = NULL;
s_entry_prefix_label = NULL;
s_suggestion_row = NULL;
s_key_matrix = NULL;
s_checksum_panel = NULL;
s_approval_countdown = NULL;
s_match_total = 0;
}
static void style_screen(lv_obj_t *scr) static void style_screen(lv_obj_t *scr)
{ {
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
@@ -62,31 +93,24 @@ static void style_button(lv_obj_t *btn)
lv_obj_set_style_text_color(btn, lv_color_hex(0xFF0000), LV_STATE_FOCUSED); lv_obj_set_style_text_color(btn, lv_color_hex(0xFF0000), LV_STATE_FOCUSED);
} }
static void style_text_area(lv_obj_t *ta) static void style_key_matrix(lv_obj_t *km)
{ {
lv_obj_set_style_bg_opa(ta, LV_OPA_COVER, 0); lv_obj_set_style_bg_opa(km, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(ta, lv_color_hex(0x000000), 0); lv_obj_set_style_bg_color(km, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(ta, 2, 0); lv_obj_set_style_border_width(km, 1, 0);
lv_obj_set_style_border_color(ta, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_border_color(km, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_color(ta, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_color(ta, lv_color_hex(0xFF0000), LV_PART_CURSOR);
}
static void style_keyboard(lv_obj_t *kb) lv_obj_set_style_bg_opa(km, LV_OPA_COVER, LV_PART_ITEMS);
{ lv_obj_set_style_bg_color(km, lv_color_hex(0x000000), LV_PART_ITEMS);
lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, 0); lv_obj_set_style_border_color(km, lv_color_hex(0xFFFFFF), LV_PART_ITEMS);
lv_obj_set_style_bg_color(kb, lv_color_hex(0x000000), 0); lv_obj_set_style_border_width(km, 1, LV_PART_ITEMS);
lv_obj_set_style_border_width(kb, 1, 0); lv_obj_set_style_text_color(km, lv_color_hex(0xFFFFFF), LV_PART_ITEMS);
lv_obj_set_style_border_color(kb, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, LV_PART_ITEMS); lv_obj_set_style_border_color(km, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED);
lv_obj_set_style_bg_color(kb, lv_color_hex(0x000000), LV_PART_ITEMS); lv_obj_set_style_text_color(km, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED);
lv_obj_set_style_border_color(kb, lv_color_hex(0xFFFFFF), LV_PART_ITEMS);
lv_obj_set_style_border_width(kb, 1, LV_PART_ITEMS);
lv_obj_set_style_text_color(kb, lv_color_hex(0xFFFFFF), LV_PART_ITEMS);
lv_obj_set_style_border_color(kb, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED); lv_obj_set_style_text_color(km, lv_color_hex(0x505050), LV_PART_ITEMS | LV_STATE_DISABLED);
lv_obj_set_style_text_color(kb, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED); lv_obj_set_style_border_color(km, lv_color_hex(0x303030), LV_PART_ITEMS | LV_STATE_DISABLED);
} }
static void lvgl_tick_cb(void *arg) static void lvgl_tick_cb(void *arg)
@@ -198,85 +222,443 @@ static void on_generate(lv_event_t *e)
} }
static void build_main_menu(void); static void build_main_menu(void);
static void refresh_entry_ui(void);
static bool verify_or_prompt_checksum(void);
static void on_back_to_menu(lv_event_t *e) static const char *s_key_map[] = {
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "\n",
"z", "x", "c", "v", "b", "n", "m", "<", "",
};
static void clear_entry_state(void)
{ {
(void)e; secure_memzero(s_committed_words, sizeof(s_committed_words));
build_main_menu(); secure_memzero(s_prefix, sizeof(s_prefix));
s_word_count = 0;
s_prefix_len = 0;
s_checksum_override = false;
s_match_total = 0;
} }
static void on_use_entry(lv_event_t *e) static bool join_committed_words(char *out, size_t out_len)
{
size_t cursor = 0;
if (out == NULL || out_len == 0) {
return false;
}
out[0] = '\0';
for (size_t i = 0; i < s_word_count; ++i) {
size_t wl = strlen(s_committed_words[i]);
if (cursor != 0) {
if (cursor + 1 >= out_len) {
out[0] = '\0';
return false;
}
out[cursor++] = ' ';
}
if (cursor + wl >= out_len) {
out[0] = '\0';
return false;
}
memcpy(out + cursor, s_committed_words[i], wl);
cursor += wl;
out[cursor] = '\0';
}
return true;
}
static void commit_word(const char *word)
{
if (word == NULL || s_word_count >= UI_MNEMONIC_WORDS) {
return;
}
memset(s_committed_words[s_word_count], 0, UI_BIP39_WORD_MAX);
strncpy(s_committed_words[s_word_count], word, UI_BIP39_WORD_MAX - 1);
s_word_count++;
secure_memzero(s_prefix, sizeof(s_prefix));
s_prefix_len = 0;
s_checksum_override = false;
}
static void on_checksum_use_anyway(lv_event_t *e)
{ {
(void)e; (void)e;
if (s_entry_ta == NULL) { char joined[256];
return; memset(joined, 0, sizeof(joined));
} if (!join_committed_words(joined, sizeof(joined))) {
set_status("Mnemonic buffer error");
const char *txt = lv_textarea_get_text(s_entry_ta); secure_memzero(joined, sizeof(joined));
if (txt == NULL || txt[0] == '\0') {
set_status("Mnemonic is empty");
return;
}
if (mnemonic_validate(txt) != 0) {
set_status("Invalid mnemonic");
return; return;
} }
memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic));
strncpy(s_selected_mnemonic, txt, sizeof(s_selected_mnemonic) - 1); strncpy(s_selected_mnemonic, joined, sizeof(s_selected_mnemonic) - 1);
s_have_mnemonic = true; s_have_mnemonic = true;
s_done = true; s_done = true;
s_checksum_override = true;
secure_memzero(joined, sizeof(joined));
}
static void on_checksum_edit_words(lv_event_t *e)
{
(void)e;
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
set_status("Checksum mismatch: edit or use anyway");
refresh_entry_ui();
}
static bool verify_or_prompt_checksum(void)
{
char joined[256];
if (s_key_matrix == NULL || s_entry_words_label == NULL || s_entry_prefix_label == NULL) {
return false;
}
if (s_word_count != UI_MNEMONIC_WORDS || s_prefix_len != 0) {
set_status("Need exactly 12 complete words");
return false;
}
memset(joined, 0, sizeof(joined));
if (!join_committed_words(joined, sizeof(joined))) {
set_status("Mnemonic buffer error");
secure_memzero(joined, sizeof(joined));
return false;
}
if (mnemonic_validate(joined) == 0) {
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
set_status("Checksum valid");
memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic));
strncpy(s_selected_mnemonic, joined, sizeof(s_selected_mnemonic) - 1);
s_have_mnemonic = true;
s_done = true;
secure_memzero(joined, sizeof(joined));
return true;
}
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
s_checksum_panel = lv_obj_create(lv_scr_act());
lv_obj_set_size(s_checksum_panel, DISPLAY_WIDTH - 20, 82);
lv_obj_align(s_checksum_panel, LV_ALIGN_TOP_MID, 0, 78);
lv_obj_set_style_bg_color(s_checksum_panel, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_color(s_checksum_panel, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(s_checksum_panel, 2, 0);
lv_obj_set_style_radius(s_checksum_panel, 6, 0);
lv_obj_set_style_pad_all(s_checksum_panel, 6, 0);
lv_obj_t *txt = lv_label_create(s_checksum_panel);
lv_label_set_text(txt, "Checksum invalid. Use anyway?");
lv_obj_set_style_text_color(txt, lv_color_hex(0xFF0000), 0);
lv_obj_align(txt, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t *use_btn = lv_btn_create(s_checksum_panel);
lv_obj_set_size(use_btn, 128, 30);
lv_obj_align(use_btn, LV_ALIGN_BOTTOM_LEFT, 4, -2);
style_button(use_btn);
lv_obj_set_style_border_color(use_btn, lv_color_hex(0xFF0000), 0);
lv_obj_add_event_cb(use_btn, on_checksum_use_anyway, LV_EVENT_RELEASED, NULL);
lv_obj_t *use_lbl = lv_label_create(use_btn);
lv_label_set_text(use_lbl, "Use Anyway");
lv_obj_center(use_lbl);
lv_obj_t *edit_btn = lv_btn_create(s_checksum_panel);
lv_obj_set_size(edit_btn, 128, 30);
lv_obj_align(edit_btn, LV_ALIGN_BOTTOM_RIGHT, -4, -2);
style_button(edit_btn);
lv_obj_add_event_cb(edit_btn, on_checksum_edit_words, LV_EVENT_RELEASED, NULL);
lv_obj_t *edit_lbl = lv_label_create(edit_btn);
lv_label_set_text(edit_lbl, "Edit Words");
lv_obj_center(edit_lbl);
set_status("Checksum invalid");
secure_memzero(joined, sizeof(joined));
return false;
}
static void on_back_to_menu(lv_event_t *e)
{
(void)e;
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
clear_entry_state();
build_main_menu();
}
static void on_suggestion_tap(lv_event_t *e)
{
const char *word = (const char *)lv_event_get_user_data(e);
if (s_suggestion_row == NULL || word == NULL || s_word_count >= UI_MNEMONIC_WORDS) {
return;
}
commit_word(word);
refresh_entry_ui();
if (s_word_count == UI_MNEMONIC_WORDS) {
(void)verify_or_prompt_checksum();
}
}
static void entry_backspace_step(void)
{
if (s_prefix_len > 0) {
s_prefix_len--;
s_prefix[s_prefix_len] = '\0';
} else if (s_word_count > 0) {
s_word_count--;
memset(s_prefix, 0, sizeof(s_prefix));
strncpy(s_prefix, s_committed_words[s_word_count], sizeof(s_prefix) - 1);
s_prefix_len = strlen(s_prefix);
secure_memzero(s_committed_words[s_word_count], sizeof(s_committed_words[s_word_count]));
}
s_checksum_override = false;
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
refresh_entry_ui();
}
static void on_key_matrix(lv_event_t *e)
{
lv_obj_t *obj = lv_event_get_target(e);
if (s_key_matrix == NULL || obj != s_key_matrix) {
return;
}
uint16_t idx = lv_btnmatrix_get_selected_btn(obj);
const char *txt = lv_btnmatrix_get_btn_text(obj, idx);
if (txt == NULL || txt[0] == '\0' || txt[1] != '\0') {
return;
}
if (txt[0] == '<') {
entry_backspace_step();
return;
}
if (s_word_count >= UI_MNEMONIC_WORDS) {
set_status("Already have 12 words");
return;
}
if (s_prefix_len + 1 >= sizeof(s_prefix)) {
set_status("Word too long");
return;
}
s_prefix[s_prefix_len++] = txt[0];
s_prefix[s_prefix_len] = '\0';
s_checksum_override = false;
refresh_entry_ui();
if (s_match_total == 1) {
const char *word = mnemonic_word_at(s_match_indices[0]);
if (word != NULL) {
commit_word(word);
refresh_entry_ui();
if (s_word_count == UI_MNEMONIC_WORDS) {
(void)verify_or_prompt_checksum();
}
}
}
}
static void refresh_entry_ui(void)
{
size_t shown = 0;
char words_line[256];
char prefix_line[64];
if (s_checksum_panel != NULL) {
lv_obj_del(s_checksum_panel);
s_checksum_panel = NULL;
}
memset(words_line, 0, sizeof(words_line));
(void)join_committed_words(words_line, sizeof(words_line));
if (s_entry_words_label != NULL) {
if (s_word_count == 0) {
lv_label_set_text(s_entry_words_label, "(no words selected)");
} else {
lv_label_set_text(s_entry_words_label, words_line);
}
}
snprintf(prefix_line, sizeof(prefix_line), "Word %u/12 prefix: %s",
(unsigned)(s_word_count + 1),
(s_prefix_len > 0) ? s_prefix : "-");
if (s_entry_prefix_label != NULL) {
lv_label_set_text(s_entry_prefix_label, prefix_line);
}
if (s_suggestion_row != NULL) {
lv_obj_clean(s_suggestion_row);
if (s_prefix_len > 0 && s_word_count < UI_MNEMONIC_WORDS) {
s_match_total = mnemonic_words_with_prefix(s_prefix, s_match_indices, UI_SUGGESTION_STORE_MAX);
shown = (s_match_total > UI_SUGGESTION_VISIBLE_MAX) ? UI_SUGGESTION_VISIBLE_MAX : s_match_total;
for (size_t i = 0; i < shown; ++i) {
const char *word = mnemonic_word_at(s_match_indices[i]);
lv_obj_t *btn;
lv_obj_t *lbl;
if (word == NULL) {
continue;
}
btn = lv_btn_create(s_suggestion_row);
lv_obj_set_size(btn, 84, 26);
style_button(btn);
lv_obj_add_event_cb(btn, on_suggestion_tap, LV_EVENT_RELEASED, (void *)word);
lbl = lv_label_create(btn);
lv_label_set_text(lbl, word);
lv_obj_center(lbl);
}
if (s_match_total > shown) {
char more[24];
lv_obj_t *m = lv_label_create(s_suggestion_row);
snprintf(more, sizeof(more), "+%u more", (unsigned)(s_match_total - shown));
lv_label_set_text(m, more);
lv_obj_set_style_text_color(m, lv_color_hex(0xFFFFFF), 0);
}
if (s_match_total == 0) {
set_status("No matches");
} else {
set_status("");
}
} else {
s_match_total = 0;
}
}
if (s_key_matrix != NULL) {
const uint16_t matrix_btn_count = 27;
for (uint16_t i = 0; i < matrix_btn_count; ++i) {
const char *txt = lv_btnmatrix_get_btn_text(s_key_matrix, i);
bool enabled = false;
if (txt == NULL || txt[0] == '\0' || txt[1] != '\0') {
continue;
}
if (txt[0] == '<') {
enabled = (s_prefix_len > 0 || s_word_count > 0);
} else if (s_word_count >= UI_MNEMONIC_WORDS) {
enabled = false;
} else if (s_prefix_len == 0) {
size_t start = 0;
enabled = (mnemonic_words_for_letter(txt[0], &start) > 0);
} else {
enabled = mnemonic_letter_extends_prefix(s_prefix, s_prefix_len, txt[0]);
}
if (enabled) {
lv_btnmatrix_clear_btn_ctrl(s_key_matrix, i, LV_BTNMATRIX_CTRL_DISABLED);
} else {
lv_btnmatrix_set_btn_ctrl(s_key_matrix, i, LV_BTNMATRIX_CTRL_DISABLED);
}
}
}
} }
static void build_entry_screen(void) static void build_entry_screen(void)
{ {
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
lv_obj_t *title;
lv_obj_t *close_btn;
lv_obj_t *close_lbl;
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);
clear_entry_state();
lv_obj_t *title = lv_label_create(scr); title = lv_label_create(scr);
lv_label_set_text(title, "Enter Mnemonic"); lv_label_set_text(title, "Enter Mnemonic");
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 6); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 4);
s_entry_ta = lv_textarea_create(scr); s_entry_words_label = lv_label_create(scr);
lv_obj_set_size(s_entry_ta, DISPLAY_WIDTH - 16, 70); lv_obj_set_width(s_entry_words_label, DISPLAY_WIDTH - 16);
lv_obj_align(s_entry_ta, LV_ALIGN_TOP_MID, 0, 32); lv_label_set_long_mode(s_entry_words_label, LV_LABEL_LONG_WRAP);
lv_textarea_set_placeholder_text(s_entry_ta, "12 words separated by spaces"); lv_label_set_text(s_entry_words_label, "(no words selected)");
lv_textarea_set_one_line(s_entry_ta, false); lv_obj_set_style_text_color(s_entry_words_label, lv_color_hex(0xFFFFFF), 0);
style_text_area(s_entry_ta); lv_obj_align(s_entry_words_label, LV_ALIGN_TOP_LEFT, 8, 26);
lv_obj_t *kb = lv_keyboard_create(scr); s_entry_prefix_label = lv_label_create(scr);
lv_obj_set_size(kb, DISPLAY_WIDTH, 110); lv_label_set_text(s_entry_prefix_label, "Word 1/12 prefix: -");
lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0); lv_obj_set_style_text_color(s_entry_prefix_label, lv_color_hex(0xFFFFFF), 0);
lv_keyboard_set_textarea(kb, s_entry_ta); lv_obj_align(s_entry_prefix_label, LV_ALIGN_TOP_LEFT, 8, 66);
style_keyboard(kb);
lv_obj_t *use_btn = lv_btn_create(scr);
lv_obj_set_size(use_btn, 150, 30);
lv_obj_align(use_btn, LV_ALIGN_TOP_LEFT, 8, 110);
lv_obj_add_event_cb(use_btn, on_use_entry, LV_EVENT_RELEASED, NULL);
style_button(use_btn);
lv_obj_t *use_lbl = lv_label_create(use_btn);
lv_label_set_text(use_lbl, "Use Mnemonic");
lv_obj_center(use_lbl);
lv_obj_t *back_btn = lv_btn_create(scr);
lv_obj_set_size(back_btn, 150, 30);
lv_obj_align(back_btn, LV_ALIGN_TOP_RIGHT, -8, 110);
lv_obj_add_event_cb(back_btn, on_back_to_menu, LV_EVENT_RELEASED, NULL);
style_button(back_btn);
lv_obj_t *back_lbl = lv_label_create(back_btn);
lv_label_set_text(back_lbl, "Back");
lv_obj_center(back_lbl);
s_status_label = lv_label_create(scr); s_status_label = lv_label_create(scr);
lv_label_set_text(s_status_label, ""); lv_label_set_text(s_status_label, "");
lv_obj_set_style_text_color(s_status_label, lv_color_hex(0xFF0000), 0); lv_obj_set_style_text_color(s_status_label, lv_color_hex(0xFF0000), 0);
lv_obj_align(s_status_label, LV_ALIGN_TOP_LEFT, 8, 144); lv_obj_align(s_status_label, LV_ALIGN_TOP_LEFT, 8, 82);
s_suggestion_row = lv_obj_create(scr);
lv_obj_set_size(s_suggestion_row, DISPLAY_WIDTH - 12, 30);
lv_obj_align(s_suggestion_row, LV_ALIGN_TOP_MID, 0, 96);
lv_obj_set_style_bg_color(s_suggestion_row, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_color(s_suggestion_row, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(s_suggestion_row, 1, 0);
lv_obj_set_style_pad_all(s_suggestion_row, 2, 0);
lv_obj_set_scroll_dir(s_suggestion_row, LV_DIR_HOR);
lv_obj_set_flex_flow(s_suggestion_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(s_suggestion_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
close_btn = lv_btn_create(scr);
lv_obj_set_size(close_btn, 42, 30);
lv_obj_align(close_btn, LV_ALIGN_TOP_RIGHT, -4, 2);
style_button(close_btn);
lv_obj_add_event_cb(close_btn, on_back_to_menu, LV_EVENT_CLICKED, NULL);
close_lbl = lv_label_create(close_btn);
lv_label_set_text(close_lbl, "X");
lv_obj_center(close_lbl);
s_key_matrix = lv_btnmatrix_create(scr);
lv_obj_set_size(s_key_matrix, DISPLAY_WIDTH - 16, 100);
lv_obj_align(s_key_matrix, LV_ALIGN_TOP_MID, 0, 132);
lv_btnmatrix_set_map(s_key_matrix, s_key_map);
lv_obj_add_event_cb(s_key_matrix, on_key_matrix, LV_EVENT_VALUE_CHANGED, NULL);
style_key_matrix(s_key_matrix);
lv_obj_set_style_pad_all(s_key_matrix, 0, 0);
lv_obj_set_style_pad_gap(s_key_matrix, 0, LV_PART_ITEMS);
lv_obj_set_style_border_width(s_key_matrix, 1, LV_PART_ITEMS);
refresh_entry_ui();
} }
static void on_enter(lv_event_t *e) static void on_enter(lv_event_t *e)
@@ -316,6 +698,7 @@ static void on_use_viewed(lv_event_t *e)
static void build_view_screen(void) static void build_view_screen(void)
{ {
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);
@@ -370,6 +753,7 @@ static void on_view(lv_event_t *e)
static void build_main_menu(void) static void build_main_menu(void)
{ {
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);
@@ -424,6 +808,7 @@ void ui_show_approval_prompt(const char *caller_pubkey_hex, int kind, const char
} }
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);
@@ -535,6 +920,7 @@ ui_approval_decision_t ui_wait_for_approval(uint32_t timeout_ms)
static void show_event_log_screen(void) static void show_event_log_screen(void)
{ {
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);
@@ -579,6 +965,7 @@ void ui_show_idle(const char *npub_full, const char *mnemonic_full, const char *
} }
lv_obj_t *scr = lv_scr_act(); lv_obj_t *scr = lv_scr_act();
reset_screen_widget_refs();
lv_obj_clean(scr); lv_obj_clean(scr);
style_screen(scr); style_screen(scr);

View File

@@ -8,3 +8,8 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_FREERTOS_HZ=1000 CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# PSA crypto for ed25519 sign/verify (IDF v5.x mbedtls has no mbedtls_ed25519_*).
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
# Curve25519 / Ed25519 ECP domain parameter (already enabled, kept for clarity).
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -4,6 +4,7 @@ idf_component_register(
"display.c" "display.c"
"mnemonic.c" "mnemonic.c"
"key_derivation.c" "key_derivation.c"
"pq_crypto_firmware.c"
"bech32.c" "bech32.c"
"usb_transport.c" "usb_transport.c"
"buttons.c" "buttons.c"
@@ -23,6 +24,7 @@ idf_component_register(
REQUIRES REQUIRES
mbedtls mbedtls
secp256k1 secp256k1
pqclean
json json
espressif__esp_tinyusb espressif__esp_tinyusb
espressif__tinyusb espressif__tinyusb

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h" #include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "esp_random.h" #include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h" #include "mbedtls/md.h"
#include "mbedtls/ed25519.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "secp256k1.h" #include "secp256k1.h"
#include "secp256k1_extrakeys.h" #include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h" #include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u #define BIP32_HARDENED_FLAG 0x80000000u
typedef struct { typedef struct {
@@ -253,3 +261,316 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx); secp256k1_context_destroy(ctx);
return 0; 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.
* Use mbedtls to derive the public key. */
memcpy(privkey, derived_seed, 32);
/* mbedtls_ed25519_make_public: derive pub from priv */
/* Note: mbedtls ed25519 API may vary by version. The ESP-IDF mbedtls
* component provides mbedtls_ed25519_make_public (or via the PK API).
* We use the low-level function if available. */
int ret = mbedtls_ed25519_make_public((unsigned char *)pubkey, 32,
(const unsigned char *)privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 make_public failed: %d", ret);
memset(derived_seed, 0, sizeof(derived_seed));
memset(privkey, 0, 32);
return -1;
}
memset(derived_seed, 0, sizeof(derived_seed));
return 0;
}
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
uint8_t sig64[64]) {
/* mbedtls_ed25519_sign: sign a message (not pre-hashed) */
int ret = mbedtls_ed25519_sign((unsigned char *)sig64, 64,
(const unsigned char *)msg32, 32,
(const unsigned char *)privkey, 32,
NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 sign failed: %d", ret);
return -1;
}
return 0;
}
/* --- 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];
mbedtls_ecp_group grp;
mbedtls_mpi d;
mbedtls_ecp_point Q;
int ret;
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.
* Clamp it per RFC 7748 and derive the public key via mbedtls ECDH. */
memcpy(privkey, derived_seed, 32);
memset(derived_seed, 0, sizeof(derived_seed));
/* x25519 clamping: priv[0] &= 248, priv[31] &= 127, priv[31] |= 64 */
privkey[0] &= 248;
privkey[31] &= 127;
privkey[31] |= 64;
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_ecp_point_init(&Q);
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 group load failed: %d", ret);
goto cleanup;
}
ret = mbedtls_mpi_read_binary_le(d, privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 mpi read failed: %d", ret);
goto cleanup;
}
ret = mbedtls_ecp_mul(&grp, &Q, d, &grp.G, NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 ecp_mul failed: %d", ret);
goto cleanup;
}
/* Serialize the public key as raw 32 bytes (little-endian) */
{
size_t olen = 0;
ret = mbedtls_ecp_point_write_binary(&grp, &Q,
MBEDTLS_ECP_PF_COMPRESSED,
&olen, pubkey, 32);
if (ret != 0 || olen != 32) {
ESP_LOGE(KD_TAG, "x25519 pub serialize failed: %d", ret);
ret = -1;
}
}
cleanup:
mbedtls_ecp_group_free(&grp);
mbedtls_mpi_free(&d);
mbedtls_ecp_point_free(&Q);
return (ret == 0) ? 0 : -1;
}
/* --- 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;
}

View File

@@ -1,7 +1,61 @@
#pragma once #pragma once
#include <stddef.h>
#include <stdint.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(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 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]); 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.
* 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]);
/* --- 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);

View 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);
}

View 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 */

View File

@@ -0,0 +1,192 @@
# n_signer FPGA Signing Core
**Status:** Concept — brainstorming. No plan yet.
An FPGA-based secp256k1 signing core that provides the **maximum physical
security** possible for Nostr signing: constant-time crypto (no instruction
timing leakage), key material in FPGA fabric (no bus access to the key), and no
firmware (no malware injection surface). The FPGA is a **signing oracle** — a
small, auditable hardware module that does one thing (secp256k1 schnorr/ECDSA
signing) with side-channel resistance that software on an MCU cannot match.
## The secure element gap
Commercial secure elements (NXP JCOP, Infineon, Microchip ATECC) support NIST
curves (P-256, P-384) and RSA, but **not secp256k1** — the smart card industry
standardized on NIST curves, and secp256k1 was treated as a "Bitcoin curve"
that didn't get hardware support. This is why every Nostr/Bitcoin hardware
wallet (Coldcard, Ledger, Trezor, Keystone) uses a **general-purpose MCU**
running software secp256k1, not a secure element.
An FPGA fills this gap: it gives us **hardware-level secp256k1** without relying
on a secure element vendor to support the curve. We write the secp256k1 core
ourselves in Verilog, with full control over the timing, the key storage, and
the side-channel resistance.
## Why an FPGA
| Property | MCU (software) | FPGA (hardware) |
|---|---|---|
| Timing leakage | Branch prediction, cache, instruction timing | **None** — fixed datapath, every op takes the same cycles |
| Key storage | RAM (accessible via bus/debug) | **FPGA fabric / BRAM** (no external bus access) |
| Firmware attacks | OS, USB stack, BT stack = injection surface | **No firmware** — bitstream is the entire program |
| Debug access | JTAG/SWD can read RAM | **No debug path to key** if not routed |
| Auditability | Large codebase (thousands of lines of C) | **Small Verilog core** (~2000 lines, auditable) |
| PQ crypto | Yes (software) | No (too complex for FPGA) |
## Architecture: hybrid FPGA + MCU
The practical design is a **two-chip hybrid**: the FPGA is the signing oracle,
the MCU handles the protocol/UI/transport. The MCU sends a message hash + key
index to the FPGA over SPI; the FPGA signs with the key in fabric; the FPGA
returns the 64-byte signature. The private key never leaves the FPGA.
```mermaid
flowchart TD
subgraph Host_Side
Host[Host: laptop/phone<br/>n_signer client]
end
subgraph Signer_Device
MCU[MCU: RP2040 or nRF52840<br/>protocol + UI + transport<br/>PQ crypto in software]
FPGA[FPGA: iCE40-UP5K<br/>secp256k1 signing core<br/>ed25519 signing core<br/>SHA-256/512 cores<br/>key in BRAM]
Display[OLED / e-paper display]
Buttons[approve / deny buttons]
end
Host -->|USB / IR / NFC / BLE| MCU
MCU -->|SPI: msg_hash + key_index| FPGA
FPGA -->|SPI: 64-byte signature| MCU
MCU --> Display
Buttons --> MCU
MCU -->|response| Host
```
### Division of labor
| Function | Chip | Notes |
|---|---|---|
| Transport (USB/IR/NFC/BLE) | MCU | Ported from CYD/Teensy firmware |
| JSON-RPC dispatch | MCU | Ported from `handle_request()` |
| Auth envelope verify | MCU | secp256k1 schnorr verify (software) |
| Approval UI (display + buttons) | MCU | Ported from CYD UI |
| Mnemonic entry | MCU | BIP-39 wordlist + entry UI |
| BIP-32 / SLIP-0010 key derivation | **FPGA** | SHA-512 HMAC core + derivation FSM |
| secp256k1 schnorr sign | **FPGA** | Constant-time scalar multiply + schnorr |
| secp256k1 ECDSA sign | **FPGA** | Same scalar multiply + RFC 6979 nonce |
| ed25519 sign | **FPGA** | Curve25519 arithmetic core |
| x25519 ECDH | **FPGA** | Same curve as ed25519 |
| SHA-256 | **FPGA** | Hardware core (~1000 LUTs) |
| SHA-512 | **FPGA** | Hardware core (~2000 LUTs) — needed for BIP-32 |
| HMAC-SHA-256 | **FPGA** | SHA-256 core + FSM — for the `derive` verb |
| NIP-04 / NIP-44 encryption | MCU | AES + ChaCha20 in software |
| ML-DSA-65 / SLH-DSA-128s / ML-KEM-768 | MCU | PQClean in software (too complex for FPGA) |
| nostr_mine_event (PoW) | MCU | SHA-256 hash loop in software (or offload to FPGA) |
### Key storage in the FPGA
The mnemonic seed (64 bytes) is loaded into the FPGA's BRAM at boot (sent by
the MCU after the user enters the mnemonic). The FPGA derives secp256k1/ed25519/
x25519 keys on demand using its SHA-512 + modular arithmetic cores. The derived
private keys live in FPGA registers/BRAM and are **never readable from the SPI
interface** — the SPI interface only accepts "sign this hash with key index N"
commands and returns signatures. There is no "read key" command.
This is the key security property: **the private key is physically unreachable
from any external interface.** On an MCU, the key is in RAM and can be read via
JTAG/SWD or a firmware exploit. On the FPGA, the key is in fabric and there is
no path to it.
## FPGA board options
| Board | FPGA | LUTs | Toolchain | Price | Notes |
|---|---|---|---|---|---|
| **iCE40-UP5K** (e.g. iCEBreaker, Fomu) | iCE40UP5K | 5,300 | **Yosys + nextpnr** (open-source) | ~$15-20 | Best DIY choice. Open-source toolchain, DSP blocks, 128 KB BRAM, SPI flash. |
| **Gowin Tang Nano 9K** | GW1NR-9 | 8,640 | Yosys + nextpnr (open-source) | ~$8 | Cheapest. Newer open-source support. |
| **Lattice ECP5** (OrangeCrab, ULX3S) | LFE5U-12F / 25F / 45F | 12K-45K | Yosys + nextpnr (open-source) | ~$30-50 | More room. Supports **bitstream encryption** (important for production). |
| **Xilinx Artix-7** (Arty A7) | XC7A35T | 33,280 | Vivado (proprietary) | ~$100 | Professional. Overkill for a signer. |
**Recommendation: Lattice iCE40-UP5K for prototyping, ECP5 for production.**
The iCE40 has the most mature open-source toolchain (Yosys + nextpnr) and is
cheap. The ECP5 adds bitstream encryption (prevents bitstream cloning) for a
production device.
## The secp256k1 core (the hard part)
The secp256k1 signing core is the main development effort. It needs:
1. **256-bit modular arithmetic** over the secp256k1 prime field (p = 2²⁵⁶ - 2³² - 977):
- Modular add, subtract, multiply (Montgomery multiplication for performance)
- Modular inversion (Fermat's little theorem: a^(p-2) mod p, or extended Euclidean)
2. **Point operations** on the secp256k1 curve (y² = x³ + 7):
- Point addition (Jacobian coordinates)
- Point doubling
- Scalar multiplication (constant-time double-and-add, no conditional branches)
3. **Schnorr sign** (BIP-340):
- Deterministic nonce: k = HMAC-SHA256(d, x) where d is the key, x is the message hash
- R = k·G (point multiplication)
- e = tagged hash(R.x || P || m) (SHA-256)
- s = (k + e·x) mod n (scalar multiply + modular add)
- Signature = (R.x, s)
4. **ECDSA sign** (for the `scheme:"ecdsa"` option):
- RFC 6979 deterministic nonce: k = HMAC-SHA256(x, m) with rejection sampling
- R = k·G
- r = R.x mod n
- s = k⁻¹ · (m + r·x) mod n
- Signature = (r, s)
**Estimated size:** ~2000-5000 LUTs for the modular arithmetic + point
multiplication + schnorr/ECDSA FSM. Fits comfortably in an iCE40-UP5K (5,300
LUTs) alongside the SHA-256/512 cores and the SPI interface.
**Estimated sign time:** ~1-5 ms at 12 MHz (iCE40-UP5K typical clock). The
bottleneck is the 256-bit modular multiplication (~0.5-2 ms per multiply, ~256
multiplies per scalar multiplication). This is **much faster than software**
(the CYD's software schnorr sign takes ~10-50 ms).
## Open questions
- **Pure FPGA vs hybrid?** A pure-FPGA signer (no MCU) would implement the
entire dispatch + transport + UI in Verilog. This is extremely secure but
very hard to build (JSON parsing in Verilog is painful). The hybrid (FPGA
signing oracle + MCU protocol) is practical and still gives the key-isolation
benefit. **Lean toward hybrid.**
- **Which MCU?** RP2040 (cheap, no WiFi) or nRF52840 (low power, NFC, BLE)?
This determines the transport options (USB, IR, NFC, BLE).
- **Bitstream security:** iCE40 doesn't support bitstream encryption. If an
attacker reads the SPI flash, they get the bitstream (but not the key — the
key is loaded at runtime by the MCU, not stored in the bitstream). For
production, use ECP5 with bitstream encryption.
- **Key loading:** the MCU sends the mnemonic seed to the FPGA at boot over
SPI. Is this SPI transfer vulnerable to sniffing? It happens once, at boot,
inside the device. If the device is physically sealed, the SPI bus is not
accessible. For higher security, the FPGA could derive the key from the
mnemonic internally (the MCU sends the mnemonic string, the FPGA does
PBKDF2-HMAC-SHA512 + BIP-32 derivation in hardware).
- **ed25519 core:** worth implementing, or secp256k1-only? ed25519 is the same
field size (256-bit) but a different curve (Curve25519 vs secp256k1). The
modular arithmetic is similar but the curve operations differ. Adding
ed25519 roughly doubles the core size.
- **Open-source secp256k1 FPGA cores:** are there existing Verilog secp256k1
cores we can reuse or adapt? (There are Bitcoin mining cores, but those do
double-SHA256, not ECDSA. Academic ECDSA-on-FPGA papers exist but the code
is rarely open-sourced. We'd likely write the core from scratch.)
## Comparison to the other concepts
| | FPGA signer (hybrid) | MCU-only (CYD/Teensy) | Secure element |
|---|---|---|---|
| secp256k1 side-channel resistance | **Best** (constant-time, key in fabric) | Medium (software, timing leakage) | N/A (no secp256k1 support) |
| Key isolation | **Best** (no bus access to key) | Low (RAM, JTAG/SWD accessible) | Best (tamper-resistant) |
| Firmware attack surface | **Minimal** (no firmware on FPGA) | Large (OS, USB, BT stacks) | Minimal (fixed function) |
| PQ crypto | On MCU (software) | On MCU (software) | N/A |
| Development effort | **High** (Verilog secp256k1 core) | Low (port existing C code) | High (NDA + Java Card) |
| Cost | ~$15 FPGA + ~$4 MCU = ~$20 | ~$4-27 (MCU only) | ~$3-5 (chip only) |
| Auditability | **High** (small Verilog core, open toolchain) | Medium (large C codebase) | Low (proprietary, NDA) |
## Next steps
- Decide: hybrid (FPGA + MCU) vs pure FPGA
- Decide: iCE40-UP5K (prototype) vs ECP5 (production)
- Decide: secp256k1-only vs secp256k1 + ed25519
- Survey existing open-source secp256k1 / ECDSA FPGA cores
- Write a Verilog secp256k1 modular arithmetic core (the foundation)
- Write a plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))

View File

@@ -0,0 +1,149 @@
# n_signer IR Air-Gap Signer
**Status:** Concept — brainstorming. No plan yet.
A hardware signer that communicates with the host via **infrared light**
line-of-sight, short-range, physically directional. The signer never touches
the host electrically: no wire, no radio, no shared ground. The only channel
is modulated light through air. A small **USB receiver dongle** on the host
decodes the IR signal and presents it as a CDC-ACM serial port.
This is the strongest air-gap model in the n_signer family: the signer is
electrically isolated from the host, and the receiver dongle is a dumb
IR-to-serial bridge with no crypto, no keys, and ~200 lines of auditable
firmware.
## Concept
```mermaid
flowchart LR
Host[Host: laptop<br/>n_signer client] -->|USB CDC| Dongle[USB IR receiver dongle<br/>RP2040 + IR receiver]
Dongle -->|IR light<br/>line-of-sight| Signer[IR signer<br/>RP2040 + OLED + buttons]
Signer -->|approve/deny button| User[User]
Signer -->|IR light response| Dongle
Dongle -->|USB CDC| 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 IR wire. The receiver dongle is a transparent byte pipe — it has
no knowledge of the protocol, no keys, and no state beyond the IR-to-USB
bridge.
## Why IR
- **True air-gap** — the signer is electrically isolated from the host. No wire,
no radio, no shared ground. Host-side malware cannot reach the signer's
firmware through the communication channel.
- **Line-of-sight required** — you point the signer at the receiver. An attacker
would need to be in the same room, in the line of sight, with their own IR
transmitter. Much smaller attack surface than BT (which broadcasts
omnidirectionally to ~10 m).
- **Dumb dongle** — the USB receiver is a simple IR-to-serial bridge. ~200
lines of firmware, no crypto, no keys, fully auditable in an afternoon. If
compromised, it can only MITM the IR stream (which is already protected by the
auth envelope).
- **No BT stack** — much smaller firmware attack surface on the signer. No
pairing, no GATT, no L2CAP, no SMP.
- **Novel** — no hardware wallet uses IR for host communication. It's a
creative solution to the air-gap problem that avoids both the wire (USB) and
the radio (BT/NFC) attack surfaces.
## Hardware (preliminary)
### Signer
| Component | Candidate | Notes |
|---|---|---|
| MCU | **RP2040** (Raspberry Pi Pico) | $4, Cortex-M0+ @ 133 MHz, 264 KB SRAM, no WiFi/BT (perfect for air-gap). Enough RAM for secp256k1 + ed25519. ML-DSA-65 fits (~6 KB heap). |
| | or **nRF52840** | If you want NFC for mnemonic loading + lower power. |
| IR transceiver | **38 kHz IR LED + TSOP38238** (raw async, 115200 baud, ~$1) | Simplest. ~11 KB/s. Fine for Nostr events (~500 bytes). Slow for PQ sigs (3-8 KB → 0.3-0.7 s). |
| | or **TFBS4711 IrDA module** (~$2, up to 4 Mbps) | Faster (~400 KB/s) but harder to source + more complex protocol. |
| Display | 0.96" SSD1306 OLED (I2C, ~$2) or 1.54" e-paper | Small is fine — shows "approve kind 1 from <caller>?" |
| Input | 2-3 tactile buttons (approve/deny/back) | |
| Power | Coin cell or small LiPo | RP2040 + OLED + IR = very low power |
### USB receiver dongle
| Component | Candidate | Notes |
|---|---|---|
| MCU | **RP2040** (Pico) or **ATmega32U4** (Arduino Micro) | $4-8. Native USB device. |
| IR receiver | Matching TSOP38238 or IrDA module | Must match the signer's IR modulation. |
| USB | Native USB CDC-ACM | Presents as `/dev/ttyACM0` to the host. |
| Firmware | ~200 lines | Read IR → write USB CDC; read USB CDC → transmit IR. A dumb pipe. No crypto, no keys, no state. |
## Throughput
| IR mode | Baud | Throughput | sign_event (500 B req + 600 B resp) | ML-DSA-65 sign (3.3 KB sig) |
|---|---|---|---|---|
| Raw 38 kHz async | 115200 | ~11 KB/s | ~100 ms | ~300 ms |
| Raw 38 kHz async | 230400 | ~23 KB/s | ~50 ms | ~150 ms |
| IrDA | 4 Mbps | ~400 KB/s | ~3 ms | ~8 ms |
**Recommendation:** start with raw 38 kHz IR at 115200 baud (simplest, cheapest,
works with any IR LED + TSOP receiver). Upgrade to 230400 or IrDA if PQ
signature throughput is a bottleneck.
## Protocol
The IR link is **half-duplex** — the signer and receiver take turns
transmitting. The protocol is simple:
1. Host sends JSON-RPC request → USB CDC → dongle transmits IR.
2. Signer receives IR, parses the request, shows approval prompt.
3. User approves/denies.
4. Signer transmits IR response → dongle → USB CDC → host.
The 4-byte big-endian length-prefix framing (same as the CYD/feather) works
over IR as-is. The auth envelope protects against MITM on the IR stream.
## Security model
- **Electrical isolation:** the signer has no electrical connection to the host.
The IR link is a one-way-at-a-time optical channel.
- **Line-of-sight:** an attacker must be in the same room, in the line of sight,
with their own IR transmitter. The auth envelope + approval prompt protect
against a MITM even if the attacker intercepts the IR stream.
- **Dumb dongle:** the USB receiver has no crypto, no keys, no protocol
knowledge. It's a byte pipe. If compromised, it can only MITM the IR stream
(already protected by the auth envelope). The dongle's firmware is small
enough to audit in an afternoon.
- **No radio:** no BT, no WiFi, no NFC (unless you add NFC for mnemonic loading).
The signer emits no RF — only modulated IR light when actively transmitting.
## Open questions
- **IR modulation:** raw 38 kHz async (simplest) vs IrDA (faster, more complex)?
- **Mnemonic entry:** buttons (scroll BIP-39 words) vs NFC from phone vs
generate-on-device? On a 0.96" OLED, scrolling 2048 words is tedious but
secure.
- **PQ crypto on RP2040:** 264 KB SRAM is enough for ML-DSA-65 but SLH-DSA-128s
is tight. May need to limit the PQ algorithm set.
- **Dongle design:** separate RP2040 Pico, or integrate the IR receiver into a
custom PCB with a USB-A plug for a compact dongle?
- **Range:** raw IR with an IR LED + TSOP38238 reaches ~1-2 m line-of-sight.
Enough for "point at the dongle on your desk" but not across a room.
- **Bidirectional IR:** the signer needs both an IR LED (transmit) and a TSOP
receiver (receive). Two modules, or an IrDA transceiver module that does both?
## Comparison to the BLE wearable signer
| | IR air-gap | BLE wearable |
|---|---|---|
| Air-gap | **High (light, line-of-sight, ~1 m)** | Medium (radio, ~10 m, omnidirectional) |
| Attack surface | **Small (no BT, dumb dongle)** | Large (BT stack) |
| Host compatibility | Requires USB dongle | Universal (phones, laptops) |
| Form factor | Handheld (point at dongle) | Wearable |
| Throughput | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) | ~250 KB/s (BLE 5) |
| Novelty | **Novel (no hardware wallet uses IR)** | Conventional |
| Cost | ~$10 signer + ~$8 dongle | ~$10-15 (nRF52840 + OLED) |
## Next steps
- Decide on IR modulation (raw 38 kHz vs IrDA)
- Decide on MCU (RP2040 vs nRF52840)
- Decide on mnemonic entry method
- Decide on display (OLED vs e-paper)
- Prototype the IR link: two RP2040 Picos + IR LEDs + TSOP38238, bidirectional
byte pipe at 115200 baud
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))

View File

@@ -0,0 +1,77 @@
// Proof-of-concept: KB2040 as composite HID + WebUSB using Adafruit TinyUSB
//
// Board/IDE requirements:
// - Board: Adafruit KB2040 (RP2040)
// - Tools -> USB Stack: Adafruit TinyUSB
// - Library: Adafruit TinyUSB Library
#include <Adafruit_TinyUSB.h>
// HID report descriptor: Consumer Control for media keys
static const uint8_t desc_hid_report[] = {
TUD_HID_REPORT_DESC_CONSUMER(HID_REPORT_ID(1))
};
Adafruit_USBD_HID usb_hid;
Adafruit_USBD_WebUSB usb_web;
WEBUSB_URL_DEF(landing_url, 1 /* https */, "example.com/nsigner/kb2040");
static uint32_t last_hid_ms = 0;
static uint8_t rx_buf[64];
void setup() {
// WebUSB vendor interface
usb_web.setLandingPage(&landing_url);
usb_web.setStringDescriptor("n_signer WebUSB");
usb_web.begin();
// HID interface (media keys)
usb_hid.setPollInterval(2);
usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
usb_hid.setStringDescriptor("KB2040 Media");
usb_hid.begin();
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
// Wait for host enumeration
while (!TinyUSBDevice.mounted()) {
delay(1);
}
digitalWrite(LED_BUILTIN, HIGH);
}
void loop() {
// Demonstrate HID activity: send volume-up every 5 seconds
if (millis() - last_hid_ms >= 5000) {
last_hid_ms = millis();
if (usb_hid.ready()) {
uint16_t key = HID_USAGE_CONSUMER_VOLUME_INCREMENT;
usb_hid.sendReport16(1, key);
delay(30);
usb_hid.sendReport16(1, 0);
digitalWrite(LED_BUILTIN, LOW);
delay(80);
digitalWrite(LED_BUILTIN, HIGH);
}
}
// Demonstrate WebUSB activity: echo back received bytes with prefix
if (usb_web.available()) {
uint32_t n = usb_web.read(rx_buf, sizeof(rx_buf));
if (n > 0) {
usb_web.write((const uint8_t *)"KB2040:", 7);
usb_web.write(rx_buf, n);
usb_web.flush();
for (int i = 0; i < 2; i++) {
digitalWrite(LED_BUILTIN, LOW);
delay(50);
digitalWrite(LED_BUILTIN, HIGH);
delay(50);
}
}
}
}

View File

@@ -0,0 +1,340 @@
# KB2040 Hidden Signer — Display Screens
Reference for the on-device UI so we can iterate on screen designs.
- Panel: **SSD1327 128x128 grayscale OLED** (I2C, STEMMA QT, addr `0x3D`).
- Rendering source: [`display.cpp`](./display.cpp), driven by call sites in [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
- Grayscale levels in use:
- dim (headers / outlines): `0x04`
- bar fill: `0x08`
- bright text: `0x0F`
- Each screen historically ended with a bottom **status line** at `y=118`, drawn by `drawStatusLine()`. The status-line writes are currently commented out in the sketch (they were being overwritten by debug/diagnostic prints), so that line now stays effectively empty unless re-enabled.
The boxes below are schematic (the panel is 128x128 px, not character-aligned). They show layout intent, relative position, and the text/labels actually printed.
---
## 1. Volume View
Source: `drawVolumeView()` in [`display.cpp`](./display.cpp:60).
Top-level "media mode" screen. Header switches between `MODE: MEDIA` / `MODE: SIGNER`.
```
+--------------------------------+
| MODE: MEDIA | <- tiny, dim (size 1)
| |
| VOL | <- size 2, dim
| |
| |
| #### #### |
| ## ## # ## | <- "50" centered, size 6
| ## ## # ## | (huge digits)
| ## ## # ## |
| #### #### |
| |
| +------------------------+ | <- volume bar outline
| |########## | | fill = volume %
| +------------------------+ |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 2. Signer Menu
Source: `drawSignerMenu()` in [`display.cpp`](./display.cpp:102).
Two items; `>` marks the current selection (shown here on index 0).
```
+--------------------------------+
| SIGNER MENU | <- size 1, dim
| |
| |
| > Generate new | <- size 2, selected
| |
| |
| Enter mnemonic | <- size 2
| |
| |
| |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 3. Enter Mnemonic (in-place 2-column grid)
Source (planned): `drawEnterMnemonic()` in [`display.cpp`](./display.cpp).
Replaces the old separate `PICK FIRST LETTER` modal + `WORD PICK` modal with a
single grid that looks like the Generated Phrase screen but is filled in by the
user. Header reads `ENTER MNEMONIC`.
Layout: 2 columns x 6 rows (slots 1-6 left, 7-12 right), same geometry as the
Generated Phrase screen. All 12 numbers are shown; words are blank until entered.
A cursor marks the active slot (starts at slot 1).
```
+--------------------------------+
| ENTER MNEMONIC | <- size 1, dim
|> 1 m 7 | <- cursor '>' on active slot
| 2 8 | slot 1 building: "m"
| 3 9 |
| 4 10 |
| 5 11 |
| 6 12 |
| |
| back letter | <- footer legend (stage-dependent)
+--------------------------------+
```
### Per-word 3-stage entry (in the active slot)
Only the letters chosen so far are shown; a full word appears only at stage 3.
| Stage | Scroll wheel cycles | Active slot shows | PLAY does |
| ----- | ---------------------------------- | ----------------- | -------------------------- |
| 1 | first letter `a`..`z` | `m` | lock letter 1 -> stage 2 |
| 2 | valid 2nd letters for letter 1 | `mo` | lock letter 2 -> stage 3 |
| 3 | words matching first two letters | `mosaic` | select word -> next slot |
- After slot 12's word is selected: build phrase, run
`mnemonic_validate_basic()`, then go to Signer Ready (or INVALID modal).
### BACK behavior (context-sensitive erase)
`BACK` = the NEXT button (PREV stays unused here), consistent with other entry
screens.
| Position | BACK action |
| -------------------------------- | --------------------------------------------- |
| Stage 3 (choosing word) | erase 2nd letter -> return to stage 2 |
| Stage 2 (choosing 2nd letter) | erase 1st letter -> return to stage 1 |
| Stage 1, slot N > 1 | clear previous slot's word, move cursor back to slot N-1 at stage 1 |
| Stage 1, slot 1 | exit to Signer Menu |
### Footer legend (stage-dependent)
| Stage | PREV (left) | PLAY (center) | NEXT (right) |
| ------- | ----------- | ------------- | ------------ |
| Stage 1 | `-` | `letter` | `back` |
| Stage 2 | `-` | `letter` | `back` |
| Stage 3 | `-` | `select` | `back` |
### New wordlist helpers required (in `mnemonic_kb.cpp` / `.h`)
- `mnemonic_second_letters(char first, char *out, uint8_t *count)`
-> set of valid 2nd letters given the 1st letter.
- `mnemonic_prefix_range(char first, char second, uint16_t *start, uint16_t *count)`
-> wordlist index range for all words starting with those two letters.
(`mnemonic_letter_range()` already provides the first-letter range and can back
the stage-1 -> stage-2 transition.)
---
## 4. Mnemonic Page (Generated Phrase)
Source: `drawMnemonicPage()` in [`display.cpp`](./display.cpp:152).
Shows generated phrase in one screen: 2 columns × 6 rows (all 12 words, no spacer rows).
```
+--------------------------------+
| GENERATED PHRASE | <- size 1, dim
| 1 abandon 7 issue | <- size 1, tight rows
| 2 ability 8 jungle |
| 3 able 9 kitchen |
| 4 about 10 lemon |
| 5 above 11 mosquito |
| 6 absent 12 napkin |
| |
| PLAY confirm NEXT cancel | <- size 1
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 5. Message (generic 2-line modal)
Source: `drawMessage()` in [`display.cpp`](./display.cpp:193).
Generic title + 2 lines. Reused for several prompts.
Example — **Approval prompt** (title `APPROVAL`):
```
+--------------------------------+
| APPROVAL | <- size 1, dim
| |
| |
| PLAY=Allow | <- size 2
| |
| PREV=Deny | <- size 2
| |
| |
| |
| |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
Other current uses of this same screen:
- `PICK FIRST LETTER` with lines `First letter` / `Letter: a`
- `INVALID` with `Bad mnemonic` / `NEXT to return`
- `ERROR` with `Generate failed` / `Try again`
---
## 6. Signer Ready
Source: `drawSignerReady()` in [`display.cpp`](./display.cpp:210).
Shown while waiting for the host to request signing.
```
+--------------------------------+
| SIGNER READY | <- size 1, dim
| |
| +------------------------+ |
| | Awaiting | | <- size 2
| | | |
| | host | | <- size 2
| +------------------------+ |
| |
| NEXT/PLAY: menu | <- size 1
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## Notes / observations for redesign
- The bottom status line frequently **duplicated** the in-body hint text (e.g. Word Pick showed `ENC scroll PLAY select` both in the body and in the status line). With status writes disabled, that redundancy is gone.
- `PICK FIRST LETTER` reuses the generic Message screen rather than having a dedicated layout — a candidate for a purpose-built screen.
- All hint text (`ENC` / `PLAY` / `NEXT` / `PREV`) refers to the physical controls: rotary encoder + the PLAY/NEXT/PREV buttons.
```
---
## Screen ↔ render-function map
| Screen | Render function | display.cpp |
| --------------- | --------------------- | ----------- |
| Volume View | `drawVolumeView()` | [60](./display.cpp:60) |
| Signer Menu | `drawSignerMenu()` | [102](./display.cpp:102) |
| Word Pick | `drawWordPick()` | [123](./display.cpp:123) |
| Mnemonic Page | `drawMnemonicPage()` | [152](./display.cpp:152) |
| Message (modal) | `drawMessage()` | [193](./display.cpp:193) |
| Signer Ready | `drawSignerReady()` | [210](./display.cpp:210) |
| Status line | `drawStatusLine()` | [53](./display.cpp:53) |
---
# PROPOSAL: three-zone button legend footer
The three physical buttons sit directly below the screen in this order:
```
PREV PLAY/PAUSE NEXT
^ ^ ^
(left) (center) (right)
```
Idea: replace the free-text status line with a fixed **3-zone legend** along the
bottom of every screen. Each zone shows a single short verb for what that button
does on the current screen — left-aligned, center-aligned, right-aligned so each
label sits over its physical button. No need to print the button name; position
implies it. An empty zone means that button does nothing on that screen.
### Layout math (size 1 font, 6 px/char on a 128 px panel)
- Left zone: left-aligned at `x = 2`.
- Center zone: centered around `x = 64` (use `getTextBounds` to center).
- Right zone: right-aligned to `x = 126` (compute `x = 126 - width`).
- Budget ~7 chars per zone worst case (7 x 6 = 42 px x 3 = 126 px). Keep verbs
<= 7 chars (`select`, `confirm`, `cancel`, `back`, `deny`, `allow`, `next`,
`prev`) to guarantee no overlap.
- Optional: a thin divider line above the legend at `y ~= 112`, and 1 px vertical
ticks between zones to reinforce the 3-button mapping.
```
+--------------------------------+
| ...screen body... |
| |
| -------------------------------| <- optional divider (y~112)
| prev confirm next | <- 3 zones (left/center/right)
+--------------------------------+
^PREV ^PLAY ^NEXT (physical buttons below glass)
```
### Per-screen legend mapping (derived from `signer_mode_tick()`)
Encoder still scrolls/selects on every screen; the legend only describes the 3
buttons. `-` = button does nothing on that screen.
| Screen / state | PREV (left) | PLAY (center) | NEXT (right) |
| -------------------------------------- | ----------- | ------------- | ------------ |
| Volume / Media view | `prev` | `play` | `next` |
| Signer Menu (`SIGNER_UI_MENU`) | `-` | `select` | `-` |
| Generated Phrase (`GENERATE_REVIEW`) | `cancel` | `confirm` | `new` |
| Signer Ready (`READY`) | `-` | `-` | `menu` |
| Letter Pick (`ENTER_LETTER`) | `-` | `confirm` | `back` |
| Word Pick (`ENTER_PICK`) | `-` | `select` | `back` |
| Approval prompt (Message modal) | `deny` | `allow` | `-` |
Notes:
- `PREV` is currently unused in most signer entry screens; it is meaningful in
Media view (`prev` track), the Approval prompt (`deny`), and now the Generated
Phrase screen (`cancel`).
### Behavior change required: Generated Phrase (`SIGNER_UI_GENERATE_REVIEW`)
The legend wording above implies a **functional change** in
[`signer_mode_tick()`](./kb2040_hidden_signer.ino:657), not just labels:
| Button | Current behavior | Proposed behavior |
| ------ | --------------------------- | ------------------------------------------ |
| PREV | (unused) | `cancel` -> return to Signer Menu |
| PLAY | confirm -> save seed | `confirm` -> save seed (unchanged) |
| NEXT | cancel -> Signer Menu | `new` -> regenerate a fresh 12-word phrase and refresh this screen |
Implementation detail for `new`:
- On `s_evt_next_pressed`, call `generate_mnemonic_12(s_generated_mnemonic, ...)`
again, then `signer_enter_review_screen()` to redraw with the new words.
- On `s_evt_prev_pressed`, call `signer_enter_menu_screen()` (the old NEXT path).
> Dependency check (resolved): `s_evt_prev_pressed` already exists and is wired
> the same way as PLAY/NEXT — declared at [line 112](./kb2040_hidden_signer.ino:112),
> reset in [`update_button_events()`](./kb2040_hidden_signer.ino:560), set on PREV
> press at [line 581](./kb2040_hidden_signer.ino:581). No new plumbing needed.
### What this replaces on each screen
- Word Pick body lines `ENC scroll PLAY select` / `NEXT back` -> removed; the
encoder action stays implicit, and PLAY/NEXT move to the footer legend.
- Mnemonic Page body line `PLAY confirm NEXT cancel` -> moves to the footer
legend (frees a body row).
- Signer Ready body line `NEXT/PLAY: menu` -> becomes a single `menu` in the
NEXT zone.
- Approval modal `PLAY=Allow` / `PREV=Deny` body lines -> footer legend
(`deny` left / `allow` center).
### Implementation sketch
1. Add a helper `drawButtonLegend(const char *left, const char *center, const char *right)`
in [`display.cpp`](./display.cpp) that renders the 3 aligned zones (plus the
optional divider/ticks) at the bottom.
2. Give each screen a small static legend (or pass it in) and call
`drawButtonLegend(...)` instead of `drawStatusLine()`.
3. Remove the now-redundant in-body hint rows listed above.
4. Keep `drawStatusLine()` available (commented/disabled) for transient debug.
### Open design decisions (need your call)
1. **NEXT "back" wording**: use the word `back` vs a glyph like `<` / `<<`.
2. **Divider/ticks**: include the thin divider line + inter-zone ticks, or keep
it text-only for a cleaner look.
3. **Empty zones**: render nothing, or a subtle `·` placeholder so the user can
tell the button is intentionally inactive.
4. **Media view**: apply the same legend (`prev` / `play` / `next`), or leave the
Volume view visually as-is since its meaning is already obvious.

View File

@@ -0,0 +1,100 @@
# KB2040 Hidden Signer (HID + WebUSB)
This directory contains an Arduino firmware scaffold that implements the `kb2040_hidden_signer` plan on top of a TinyUSB composite device.
- Firmware: [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino)
- Host client: [`examples/kb2040_hidden_signer_client.py`](../../examples/kb2040_hidden_signer_client.py)
## What is implemented
1. Composite USB device with two active interfaces:
- HID Consumer Control (media keys)
- Vendor/WebUSB transport (framed JSON)
2. Default media mode:
- Play/Pause, Next, Previous buttons emit HID consumer usages
- Rotary encoder emits volume up/down
3. Hidden mode toggle:
- Hold PLAY + NEXT for 3 seconds to toggle media/signer mode
4. WebUSB framed transport:
- 4-byte big-endian length + JSON payload
5. Signer RPC scaffold methods:
- `ping`
- `get_status`
- `set_mnemonic`
- `set_auto_approve`
- `get_public_key` (deterministic placeholder pubkey)
- `sign_event` (approval gate + placeholder signature)
## What remains to reach production n_signer parity
1. Replace placeholder crypto with real `n_signer` core integration:
- mnemonic -> seed -> key derivation
- secp256k1 schnorr signing
- full auth envelope verification
2. Replace naive JSON parsing with robust parsing (e.g. `ArduinoJson` or cJSON port).
3. Wire exact `keyboard_modules` pin mapping and display abstraction.
4. Add secure memory wiping and persistent policy storage if required.
## Arduino setup (IDE)
1. Install Adafruit board index in Arduino IDE.
2. Install **Adafruit RP2040** boards package.
3. Select board **Adafruit KB2040**.
4. Set **Tools -> USB Stack -> Adafruit TinyUSB**.
5. Install library **Adafruit TinyUSB Library**.
6. Open and flash [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
## Arduino CLI flashing (recommended repeatable command)
Use this exact command when flashing from CLI:
```bash
arduino-cli compile --upload -p /dev/ttyACM0 \
--fqbn rp2040:rp2040:adafruit_kb2040:usbstack=tinyusb \
firmware/kb2040_hidden_signer
```
Notes:
- `usbstack=tinyusb` is **required** for this firmware (TinyUSB composite device).
- If you omit that option, build fails with `TinyUSB is not selected`.
- If `/dev/ttyACM0` changes, find the current port with:
```bash
arduino-cli board list
```
## Host validation
Install dependency:
```bash
pip install pyusb
```
Basic dual-interface verification:
```bash
python3 tests/test_kb2040_dual.py
```
Signer RPC flow (WebUSB/vendor interface):
```bash
python3 examples/kb2040_hidden_signer_client.py status
python3 examples/kb2040_hidden_signer_client.py set-mnemonic --mnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon 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"}'
```
## Pin mapping
Current defaults in [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino):
- `PIN_BTN_PLAY = 2`
- `PIN_BTN_NEXT = 3`
- `PIN_BTN_PREV = 4`
- `PIN_ENC_A = 5`
- `PIN_ENC_B = 6`
- `PIN_ENC_SW = 7`
Update these constants to match your physical `keyboard_modules` build.

View File

@@ -0,0 +1,467 @@
#include "display.h"
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1327.h>
namespace {
static constexpr uint8_t kDisplayWidth = 128;
static constexpr uint8_t kDisplayHeight = 128;
static constexpr uint8_t kDefaultI2cAddr = 0x3D;
static constexpr uint8_t kGrayDim = 0x04;
static constexpr uint8_t kGrayBar = 0x08;
static constexpr uint8_t kGrayText = 0x0F;
Adafruit_SSD1327 g_display(kDisplayWidth, kDisplayHeight, &Wire, -1, 400000, 100000);
bool g_displayPresent = false;
bool g_dirty = true;
display_view_t g_view = DISPLAY_VIEW_VOLUME;
uint8_t g_volume = 50;
bool g_signerMode = false;
char g_status[24] = "BOOT";
uint8_t g_menuSelected = 0;
uint8_t g_mnemonicPage = 0;
char g_mnemonic[256] = "";
char g_msgTitle[18] = "";
char g_msgLine1[22] = "";
char g_msgLine2[22] = "";
char g_enterSlots[12][16] = {{0}};
uint8_t g_enterActiveSlot = 0;
uint8_t g_enterStage = 1;
char g_enterBuilding[16] = "";
uint8_t g_enterCursorPos = 0;
enum signer_screen_t {
SIGNER_SCREEN_MENU = 0,
SIGNER_SCREEN_ENTER_GRID = 1,
SIGNER_SCREEN_MNEMONIC_PAGE = 2,
SIGNER_SCREEN_MESSAGE = 3,
SIGNER_SCREEN_READY = 4,
};
signer_screen_t g_signerScreen = SIGNER_SCREEN_MENU;
uint8_t clampVolume(uint8_t v) {
return (v > 100) ? 100 : v;
}
void drawStatusLine() {
g_display.setTextSize(1);
g_display.setTextColor(kGrayText);
g_display.setCursor(2, 118);
g_display.print(g_status);
}
void drawButtonLegend(const char *left, const char *center, const char *right) {
g_display.setTextSize(1);
g_display.setTextColor(kGrayText);
g_display.setTextWrap(false);
if (left && left[0] != '\0') {
g_display.setCursor(2, 118);
g_display.print(left);
}
int16_t x1 = 0;
int16_t y1 = 0;
uint16_t w = 0;
uint16_t h = 0;
if (center && center[0] != '\0') {
g_display.getTextBounds(center, 0, 0, &x1, &y1, &w, &h);
const int16_t x = static_cast<int16_t>((kDisplayWidth - w) / 2);
g_display.setCursor(x, 118);
g_display.print(center);
}
if (right && right[0] != '\0') {
g_display.getTextBounds(right, 0, 0, &x1, &y1, &w, &h);
const int16_t x = static_cast<int16_t>(126 - w);
g_display.setCursor(x, 118);
g_display.print(right);
}
}
void drawVolumeView() {
g_display.setTextWrap(false);
g_display.setTextColor(kGrayDim);
g_display.setTextSize(2);
g_display.setCursor(40, 18);
g_display.print("VOL");
char volBuf[4];
snprintf(volBuf, sizeof(volBuf), "%u", g_volume);
int16_t x1 = 0;
int16_t y1 = 0;
uint16_t w = 0;
uint16_t h = 0;
g_display.setTextSize(6);
g_display.getTextBounds(volBuf, 0, 0, &x1, &y1, &w, &h);
const int16_t x = static_cast<int16_t>((kDisplayWidth - w) / 2);
const int16_t y = 42;
g_display.setTextColor(kGrayText);
g_display.setCursor(x, y);
g_display.print(volBuf);
const int16_t barX = 8;
const int16_t barY = 102;
const int16_t barW = kDisplayWidth - 16;
const int16_t barH = 8;
g_display.drawRect(barX, barY, barW, barH, kGrayDim);
const int16_t fillW = static_cast<int16_t>((static_cast<uint32_t>(barW - 2) * g_volume) / 100U);
if (fillW > 0) {
g_display.fillRect(barX + 1, barY + 1, fillW, barH - 2, kGrayBar);
}
drawButtonLegend("prev", "play", "next");
}
void drawSignerMenu() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
g_display.print("SIGNER MENU");
const char *items[2] = {"Generate new", "Enter mnemonic"};
g_display.setTextColor(kGrayText);
g_display.setTextSize(2);
for (uint8_t i = 0; i < 2; ++i) {
g_display.setCursor(6, 28 + i * 24);
g_display.print((i == g_menuSelected) ? ">" : " ");
g_display.setCursor(20, 28 + i * 24);
g_display.print(items[i]);
}
drawButtonLegend("", "select", "");
}
void drawEnterMnemonic() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
g_display.print("ENTER MNEMONIC");
g_display.setTextColor(kGrayText);
for (uint8_t i = 0; i < 12; ++i) {
const uint8_t row = (i < 6) ? i : (i - 6);
const int16_t x = (i < 6) ? 0 : 64;
const int16_t y = static_cast<int16_t>(18 + row * 9);
const bool isActive = (i == g_enterActiveSlot);
const char cursor = isActive ? '>' : ' ';
const char *slotWord = g_enterSlots[i];
if (!slotWord) {
slotWord = "";
}
char activeBuf[16];
const char *shownWord = slotWord;
if (isActive) {
const char *src = g_enterBuilding;
if (!src) {
src = "";
}
if (g_enterStage < 3) {
size_t srcLen = strlen(src);
if (srcLen > 14) {
srcLen = 14;
}
size_t pos = g_enterCursorPos;
if (pos > srcLen) {
pos = srcLen;
}
size_t out = 0;
for (size_t j = 0; j < pos && out < sizeof(activeBuf) - 1; ++j) {
activeBuf[out++] = src[j];
}
if (out < sizeof(activeBuf) - 1) {
activeBuf[out++] = '_';
}
for (size_t j = pos; j < srcLen && out < sizeof(activeBuf) - 1; ++j) {
activeBuf[out++] = src[j];
}
activeBuf[out] = '\0';
shownWord = activeBuf;
} else if (src[0] != '\0') {
shownWord = src;
}
}
char lineBuf[22];
snprintf(lineBuf, sizeof(lineBuf), "%c%2u %s", cursor, (unsigned)(i + 1), shownWord);
g_display.setCursor(x, y);
g_display.print(lineBuf);
}
if (g_enterStage == 3) {
drawButtonLegend("", "select", "back");
} else {
drawButtonLegend("", "letter", "back");
}
}
void drawMnemonicPage() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
g_display.print("GENERATED PHRASE");
char copy[256];
strncpy(copy, g_mnemonic, sizeof(copy) - 1);
copy[sizeof(copy) - 1] = '\0';
char *words[12] = {nullptr};
uint8_t count = 0;
char *saveptr = nullptr;
char *tok = strtok_r(copy, " ", &saveptr);
while (tok && count < 12) {
words[count++] = tok;
tok = strtok_r(nullptr, " ", &saveptr);
}
g_display.setTextColor(kGrayText);
for (uint8_t i = 0; i < count; ++i) {
const uint8_t row = (i < 6) ? i : (i - 6);
const int16_t x = (i < 6) ? 0 : 64;
const int16_t y = static_cast<int16_t>(18 + row * 9);
char lineBuf[20];
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(i + 1), words[i]);
g_display.setCursor(x, y);
g_display.print(lineBuf);
}
drawButtonLegend("cancel", "confirm", "new");
}
void drawMessage() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
g_display.print(g_msgTitle);
g_display.setTextSize(2);
g_display.setTextColor(kGrayText);
g_display.setCursor(6, 34);
g_display.print(g_msgLine1);
g_display.setCursor(6, 62);
g_display.print(g_msgLine2);
const char *left = "";
const char *center = "";
const char *right = "";
if (strcmp(g_msgTitle, "APPROVAL") == 0) {
left = "deny";
center = "allow";
} else if (strcmp(g_msgTitle, "INVALID") == 0) {
center = "select";
right = "back";
} else if (strcmp(g_msgTitle, "ERROR") == 0) {
center = "select";
}
drawButtonLegend(left, center, right);
}
void drawSignerReady() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
g_display.print("SIGNER READY");
g_display.drawRect(4, 22, 120, 62, kGrayDim);
g_display.setTextSize(2);
g_display.setTextColor(kGrayText);
g_display.setCursor(16, 36);
g_display.print("Awaiting");
g_display.setCursor(30, 58);
g_display.print("host");
drawButtonLegend("", "", "menu");
}
void renderFrame() {
if (!g_displayPresent) {
return;
}
g_display.clearDisplay();
if (g_view == DISPLAY_VIEW_VOLUME) {
drawVolumeView();
} else {
switch (g_signerScreen) {
case SIGNER_SCREEN_MENU:
drawSignerMenu();
break;
case SIGNER_SCREEN_ENTER_GRID:
drawEnterMnemonic();
break;
case SIGNER_SCREEN_MNEMONIC_PAGE:
drawMnemonicPage();
break;
case SIGNER_SCREEN_MESSAGE:
drawMessage();
break;
case SIGNER_SCREEN_READY:
drawSignerReady();
break;
default:
drawMessage();
break;
}
}
g_display.display();
}
void copy_text(char *dst, size_t dst_sz, const char *src) {
if (!dst || dst_sz == 0) {
return;
}
if (!src) {
dst[0] = '\0';
return;
}
strncpy(dst, src, dst_sz - 1);
dst[dst_sz - 1] = '\0';
}
} // namespace
void display_init() {
Wire.setClock(400000);
Wire.begin();
Wire.beginTransmission(kDefaultI2cAddr);
const uint8_t probeErr = Wire.endTransmission();
if (probeErr != 0) {
g_displayPresent = false;
return;
}
g_displayPresent = g_display.begin(kDefaultI2cAddr, true);
if (!g_displayPresent) {
return;
}
g_dirty = true;
renderFrame();
}
bool display_is_present() {
return g_displayPresent;
}
void display_set_view(display_view_t view) {
if (g_view == view) {
return;
}
g_view = view;
g_dirty = true;
}
void display_set_mode(bool signer_mode) {
if (g_signerMode == signer_mode) {
return;
}
g_signerMode = signer_mode;
g_dirty = true;
}
void display_set_volume(uint8_t volume) {
const uint8_t v = clampVolume(volume);
if (v == g_volume) {
return;
}
g_volume = v;
g_dirty = true;
}
void display_show_signer_menu(uint8_t selected_index) {
g_signerScreen = SIGNER_SCREEN_MENU;
g_menuSelected = selected_index > 1 ? 1 : selected_index;
g_dirty = true;
}
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
const char *building_text, uint8_t cursor_pos) {
g_signerScreen = SIGNER_SCREEN_ENTER_GRID;
g_enterActiveSlot = (active_slot > 11) ? 11 : active_slot;
g_enterStage = stage;
g_enterCursorPos = cursor_pos;
copy_text(g_enterBuilding, sizeof(g_enterBuilding), building_text);
for (uint8_t i = 0; i < 12; ++i) {
const char *src = "";
if (slots && slots[i]) {
src = slots[i];
}
copy_text(g_enterSlots[i], sizeof(g_enterSlots[i]), src);
}
g_dirty = true;
}
void display_show_mnemonic_page(const char *mnemonic, uint8_t page_index) {
g_signerScreen = SIGNER_SCREEN_MNEMONIC_PAGE;
g_mnemonicPage = page_index;
copy_text(g_mnemonic, sizeof(g_mnemonic), mnemonic);
g_dirty = true;
}
void display_show_message(const char *title, const char *line1, const char *line2) {
g_signerScreen = SIGNER_SCREEN_MESSAGE;
copy_text(g_msgTitle, sizeof(g_msgTitle), title);
copy_text(g_msgLine1, sizeof(g_msgLine1), line1);
copy_text(g_msgLine2, sizeof(g_msgLine2), line2);
g_dirty = true;
}
void display_show_signer_ready() {
g_signerScreen = SIGNER_SCREEN_READY;
g_dirty = true;
}
// Note: status-line updates are currently disabled at call sites in
// kb2040_hidden_signer.ino by commenting out display_set_status(...)
// invocations to keep the bottom status line from being overwritten.
void display_set_status(const char *status) {
if (!status) {
return;
}
copy_text(g_status, sizeof(g_status), status);
g_dirty = true;
}
void display_tick() {
if (!g_displayPresent || !g_dirty) {
return;
}
renderFrame();
g_dirty = false;
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
enum display_view_t {
DISPLAY_VIEW_VOLUME = 0,
DISPLAY_VIEW_SIGNER = 1,
};
// Initialize the SSD1327 OLED on KB2040 STEMMA QT (Wire = Wire0, GPIO12/13).
void display_init();
// Returns true when the display was detected and initialized successfully.
bool display_is_present();
// Select top-level display view.
void display_set_view(display_view_t view);
// Set current mode text for volume view header.
void display_set_mode(bool signer_mode);
// Set local volume value shown on-screen. Input is clamped to 0..100.
void display_set_volume(uint8_t volume);
// Signer UI rendering helpers.
void display_show_signer_menu(uint8_t selected_index);
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
const char *building_text, uint8_t cursor_pos);
void display_show_mnemonic_page(const char *mnemonic, uint8_t page_index);
void display_show_message(const char *title, const char *line1, const char *line2);
void display_show_signer_ready();
// Set short status text line (truncated to fit).
void display_set_status(const char *status);
// Non-blocking tick; renders only when state changed.
void display_tick();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
#include "mnemonic_kb.h"
#include <Arduino.h>
#include <string.h>
#include "mnemonic_wordlist.h"
namespace {
static bool s_rng_seeded = false;
void ensure_rng_seeded() {
if (s_rng_seeded) {
return;
}
uint32_t seed = micros() ^ (millis() << 16);
for (int i = 0; i < 16; ++i) {
seed ^= ((uint32_t)analogRead(A0) & 0x3FFu) << (i % 22);
delayMicroseconds(50);
}
randomSeed(seed);
s_rng_seeded = true;
}
bool append_word(char* out, size_t out_len, size_t* cursor, const char* word) {
const size_t wlen = strlen(word);
if (*cursor >= out_len) {
return false;
}
if (*cursor != 0) {
if (*cursor + 1 >= out_len) {
return false;
}
out[*cursor] = ' ';
(*cursor)++;
}
if (*cursor + wlen >= out_len) {
return false;
}
memcpy(out + *cursor, word, wlen);
*cursor += wlen;
out[*cursor] = '\0';
return true;
}
} // namespace
bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count) {
if (!out_start || !out_count) {
return false;
}
if (letter >= 'A' && letter <= 'Z') {
letter = (char)(letter - 'A' + 'a');
}
if (letter < 'a' || letter > 'z') {
return false;
}
int first = -1;
int last = -1;
for (int i = 0; i < 2048; ++i) {
const char c = BIP39_WORDLIST[i][0];
if (c == letter) {
if (first < 0) first = i;
last = i;
} else if (first >= 0) {
break;
}
}
if (first < 0 || last < first) {
return false;
}
*out_start = (uint16_t)first;
*out_count = (uint16_t)(last - first + 1);
return true;
}
bool mnemonic_second_letters(char first, char *out, uint8_t *count) {
if (!out || !count) {
return false;
}
*count = 0;
if (first >= 'A' && first <= 'Z') {
first = (char)(first - 'A' + 'a');
}
if (first < 'a' || first > 'z') {
return false;
}
uint16_t start = 0;
uint16_t range_count = 0;
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
return false;
}
bool seen[26] = {false};
uint8_t found = 0;
for (uint16_t i = 0; i < range_count; ++i) {
const char* w = BIP39_WORDLIST[start + i];
char c = w[1];
if (c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
if (c < 'a' || c > 'z') {
continue;
}
const uint8_t idx = (uint8_t)(c - 'a');
if (!seen[idx]) {
seen[idx] = true;
out[found++] = c;
}
}
*count = found;
return found > 0;
}
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count) {
if (!out_start || !out_count) {
return false;
}
if (first >= 'A' && first <= 'Z') {
first = (char)(first - 'A' + 'a');
}
if (second >= 'A' && second <= 'Z') {
second = (char)(second - 'A' + 'a');
}
if (first < 'a' || first > 'z' || second < 'a' || second > 'z') {
return false;
}
uint16_t start = 0;
uint16_t range_count = 0;
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
return false;
}
int first_match = -1;
int last_match = -1;
for (uint16_t i = 0; i < range_count; ++i) {
const char* w = BIP39_WORDLIST[start + i];
char c = w[1];
if (c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
if (c == second) {
if (first_match < 0) {
first_match = (int)(start + i);
}
last_match = (int)(start + i);
} else if (first_match >= 0) {
break;
}
}
if (first_match < 0 || last_match < first_match) {
return false;
}
*out_start = (uint16_t)first_match;
*out_count = (uint16_t)(last_match - first_match + 1);
return true;
}
const char* mnemonic_word_at(uint16_t index) {
if (index >= 2048) {
return nullptr;
}
return BIP39_WORDLIST[index];
}
int mnemonic_word_index(const char* word) {
if (!word || !word[0]) {
return -1;
}
for (int i = 0; i < 2048; ++i) {
if (strcmp(word, BIP39_WORDLIST[i]) == 0) {
return i;
}
}
return -1;
}
int generate_mnemonic_12(char* out, size_t out_len) {
if (!out || out_len == 0) {
return -1;
}
out[0] = '\0';
ensure_rng_seeded();
size_t cursor = 0;
for (int i = 0; i < 12; ++i) {
const uint16_t idx = (uint16_t)random(0, 2048);
const char* word = mnemonic_word_at(idx);
if (!word) {
return -1;
}
if (!append_word(out, out_len, &cursor, word)) {
return -1;
}
}
return 0;
}
bool mnemonic_validate_basic(const char* mnemonic) {
if (!mnemonic || !mnemonic[0]) {
return false;
}
char buf[256];
strncpy(buf, mnemonic, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
int count = 0;
char* saveptr = nullptr;
char* tok = strtok_r(buf, " ", &saveptr);
while (tok) {
if (mnemonic_word_index(tok) < 0) {
return false;
}
++count;
tok = strtok_r(nullptr, " ", &saveptr);
}
return count == 12;
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
const char* mnemonic_word_at(uint16_t index);
int mnemonic_word_index(const char* word);
bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count);
bool mnemonic_second_letters(char first, char *out, uint8_t *count);
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count);
int generate_mnemonic_12(char* out, size_t out_len);
bool mnemonic_validate_basic(const char* mnemonic);

View 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"
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
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.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 18
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* Limits the length of circular references can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_CIRCULAR_LIMIT
#define CJSON_CIRCULAR_LIMIT 10000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable address area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
#define cJSON_SetBoolValue(object, boolValue) ( \
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
cJSON_Invalid\
)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,320 @@
#include "../../secp256k1/include/secp256k1.h"
#include "../../secp256k1/include/secp256k1_schnorrsig.h"
#include "../../secp256k1/include/secp256k1_ecdh.h"
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include "../nostr_platform.h"
/*
* PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED
* These functions are for internal library use only.
*/
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
// Global context for secp256k1 operations
static secp256k1_context* g_ctx = NULL;
int nostr_secp256k1_context_create(void) {
if (g_ctx != NULL) {
return 1; // Already initialized
}
g_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
if (g_ctx == NULL) {
return 0;
}
// Add randomization to the context using platform entropy.
unsigned char randomize[32];
if (nostr_platform_random(randomize, sizeof(randomize)) != 0) {
secp256k1_context_destroy(g_ctx);
g_ctx = NULL;
return 0;
}
if (!secp256k1_context_randomize(g_ctx, randomize)) {
secp256k1_context_destroy(g_ctx);
g_ctx = NULL;
return 0;
}
return 1;
}
void nostr_secp256k1_context_destroy(void) {
if (g_ctx != NULL) {
secp256k1_context_destroy(g_ctx);
g_ctx = NULL;
}
}
int nostr_secp256k1_ec_seckey_verify(const unsigned char *seckey) {
if (g_ctx == NULL || seckey == NULL) {
return 0;
}
return secp256k1_ec_seckey_verify(g_ctx, seckey);
}
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey) {
if (g_ctx == NULL || pubkey == NULL || seckey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
if (!secp256k1_ec_pubkey_create(g_ctx, &internal_pubkey, seckey)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair *keypair, const unsigned char *seckey) {
if (g_ctx == NULL || keypair == NULL || seckey == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
if (!secp256k1_keypair_create(g_ctx, &internal_keypair, seckey)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(keypair->data, &internal_keypair, sizeof(secp256k1_keypair));
return 1;
}
int nostr_secp256k1_keypair_xonly_pub(nostr_secp256k1_xonly_pubkey *pubkey, const nostr_secp256k1_keypair *keypair) {
if (g_ctx == NULL || pubkey == NULL || keypair == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_keypair, keypair->data, sizeof(secp256k1_keypair));
if (!secp256k1_keypair_xonly_pub(g_ctx, &internal_xonly, NULL, &internal_keypair)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_xonly, sizeof(secp256k1_xonly_pubkey));
return 1;
}
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey *pubkey, const unsigned char *input32) {
if (g_ctx == NULL || pubkey == NULL || input32 == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
if (!secp256k1_xonly_pubkey_parse(g_ctx, &internal_xonly, input32)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_xonly, sizeof(secp256k1_xonly_pubkey));
return 1;
}
int nostr_secp256k1_xonly_pubkey_serialize(unsigned char *output32, const nostr_secp256k1_xonly_pubkey *pubkey) {
if (g_ctx == NULL || output32 == NULL || pubkey == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_xonly, pubkey->data, sizeof(secp256k1_xonly_pubkey));
return secp256k1_xonly_pubkey_serialize(g_ctx, output32, &internal_xonly);
}
int nostr_secp256k1_schnorrsig_sign32(unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_keypair *keypair, const unsigned char *aux_rand32) {
if (g_ctx == NULL || sig64 == NULL || msghash32 == NULL || keypair == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
// Copy from our wrapper to internal representation
memcpy(&internal_keypair, keypair->data, sizeof(secp256k1_keypair));
return secp256k1_schnorrsig_sign32(g_ctx, sig64, msghash32, &internal_keypair, aux_rand32);
}
int nostr_secp256k1_schnorrsig_verify(const unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_xonly_pubkey *pubkey) {
if (g_ctx == NULL || sig64 == NULL || msghash32 == NULL || pubkey == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_xonly, pubkey->data, sizeof(secp256k1_xonly_pubkey));
return secp256k1_schnorrsig_verify(g_ctx, sig64, msghash32, 32, &internal_xonly);
}
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char *output, const nostr_secp256k1_pubkey *pubkey) {
if (g_ctx == NULL || output == NULL || pubkey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
size_t outputlen = 33;
// Copy from our wrapper to internal representation
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
return secp256k1_ec_pubkey_serialize(g_ctx, output, &outputlen, &internal_pubkey, SECP256K1_EC_COMPRESSED);
}
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak) {
if (g_ctx == NULL || seckey == NULL || tweak == NULL) {
return 0;
}
return secp256k1_ec_seckey_tweak_add(g_ctx, seckey, tweak);
}
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen) {
if (g_ctx == NULL || pubkey == NULL || input == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
if (!secp256k1_ec_pubkey_parse(g_ctx, &internal_pubkey, input, inputlen)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey, void *hashfp, void *data) {
if (g_ctx == NULL || result == NULL || pubkey == NULL || seckey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
// Copy from our wrapper to internal representation
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
}
int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32) {
if (g_ctx == NULL || pubkey == NULL || tweak32 == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_tweak_mul(g_ctx, &internal_pubkey, tweak32)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey) {
if (g_ctx == NULL || pubkey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_negate(g_ctx, &internal_pubkey)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
const nostr_secp256k1_pubkey* const* ins,
size_t n) {
if (g_ctx == NULL || out == NULL || ins == NULL || n == 0) {
return 0;
}
secp256k1_pubkey* parsed = (secp256k1_pubkey*)calloc(n, sizeof(secp256k1_pubkey));
const secp256k1_pubkey** ptrs = (const secp256k1_pubkey**)calloc(n, sizeof(secp256k1_pubkey*));
if (!parsed || !ptrs) {
free(parsed);
free(ptrs);
return 0;
}
for (size_t i = 0; i < n; i++) {
if (!ins[i]) {
free(parsed);
free(ptrs);
return 0;
}
memcpy(&parsed[i], ins[i]->data, sizeof(secp256k1_pubkey));
ptrs[i] = &parsed[i];
}
secp256k1_pubkey combined;
int ok = secp256k1_ec_pubkey_combine(g_ctx, &combined, ptrs, n);
free(parsed);
free(ptrs);
if (!ok) {
return 0;
}
memcpy(out->data, &combined, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return 0;
}
if (nostr_platform_random(buf, len) != 0) {
return 0;
}
return 1;
}

View File

@@ -0,0 +1,345 @@
/*
* NOSTR Core Library - NIP-001: Basic Protocol Flow
*
* Event creation, signing, serialization and core protocol functions
*/
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nostr_common.h"
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
typedef struct {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey* pubkey, const unsigned char* input32);
int nostr_secp256k1_schnorrsig_verify(const unsigned char* sig64, const unsigned char* msg32, const nostr_secp256k1_xonly_pubkey* pubkey);
// Declare utility functions
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex);
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len);
int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash);
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature);
/**
* Create and sign a NOSTR event
*/
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
if (!private_key) {
return NULL;
}
if (!content) {
content = ""; // Default to empty content
}
// Convert private key to public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NULL;
}
// Convert public key to hex
char pubkey_hex[65];
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
// Create event structure
cJSON* event = cJSON_CreateObject();
if (!event) {
return NULL;
}
// Use provided timestamp or current time if timestamp is 0
time_t event_time = (timestamp == 0) ? time(NULL) : timestamp;
cJSON_AddStringToObject(event, "pubkey", pubkey_hex);
cJSON_AddNumberToObject(event, "created_at", (double)event_time);
cJSON_AddNumberToObject(event, "kind", kind);
// Add tags (copy provided tags or create empty array)
if (tags) {
cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1));
} else {
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
}
cJSON_AddStringToObject(event, "content", content);
// ============================================================================
// INLINE SERIALIZATION AND SIGNING LOGIC
// ============================================================================
// Get event fields for serialization
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
cJSON_Delete(event);
return NULL;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
cJSON_Delete(event);
return NULL;
}
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
cJSON_Delete(event);
return NULL;
}
// Hash the serialized event
unsigned char event_hash[32];
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
free(serialize_string);
cJSON_Delete(event);
return NULL;
}
// Convert hash to hex for event ID
char event_id[65];
nostr_bytes_to_hex(event_hash, 32, event_id);
// Sign the hash using ECDSA
unsigned char signature[64];
if (nostr_ec_sign(private_key, event_hash, signature) != 0) {
free(serialize_string);
cJSON_Delete(event);
return NULL;
}
// Convert signature to hex
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
// Add ID and signature to the event
cJSON_AddStringToObject(event, "id", event_id);
cJSON_AddStringToObject(event, "sig", sig_hex);
free(serialize_string);
return event;
}
/**
* Validate the structure of a NOSTR event
* Checks required fields, types, and basic format validation
*/
int nostr_validate_event_structure(cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Check required fields exist
cJSON* id_item = cJSON_GetObjectItem(event, "id");
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Validate field types
if (!cJSON_IsString(id_item)) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!cJSON_IsString(pubkey_item)) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!cJSON_IsNumber(created_at_item)) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
if (!cJSON_IsNumber(kind_item)) return NOSTR_ERROR_EVENT_INVALID_KIND;
if (!cJSON_IsArray(tags_item)) return NOSTR_ERROR_EVENT_INVALID_TAGS;
if (!cJSON_IsString(content_item)) return NOSTR_ERROR_EVENT_INVALID_CONTENT;
if (!cJSON_IsString(sig_item)) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex string lengths
const char* id_str = cJSON_GetStringValue(id_item);
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!id_str || strlen(id_str) != 64) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!pubkey_str || strlen(pubkey_str) != 64) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!sig_str || strlen(sig_str) != 128) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex characters (lowercase)
for (int i = 0; i < 64; i++) {
char c = id_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_ID;
}
c = pubkey_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
}
// Validate signature hex characters (lowercase) - 128 characters
for (int i = 0; i < 128; i++) {
char c = sig_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
}
// Validate created_at is a valid timestamp (positive number)
double created_at = cJSON_GetNumberValue(created_at_item);
if (created_at < 0) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
// Validate kind is valid (0-65535)
double kind = cJSON_GetNumberValue(kind_item);
if (kind < 0 || kind > 65535 || kind != (int)kind) {
return NOSTR_ERROR_EVENT_INVALID_KIND;
}
// Validate tags array structure (array of arrays of strings)
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
cJSON* tag_element;
cJSON_ArrayForEach(tag_element, tag_item) {
if (!cJSON_IsString(tag_element)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
}
}
return NOSTR_SUCCESS;
}
/**
* Verify the cryptographic signature of a NOSTR event
* Validates event ID and signature according to NIP-01
*/
int nostr_verify_event_signature(cJSON* event) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Get event fields
cJSON* id_item = cJSON_GetObjectItem(event, "id");
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
return NOSTR_ERROR_MEMORY_FAILED;
}
// Hash the serialized event
unsigned char event_hash[32];
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
free(serialize_string);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Convert hash to hex for event ID verification
char calculated_id[65];
nostr_bytes_to_hex(event_hash, 32, calculated_id);
// Compare with provided event ID
const char* provided_id = cJSON_GetStringValue(id_item);
if (!provided_id || strcmp(calculated_id, provided_id) != 0) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_ID;
}
// Verify signature
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!pubkey_str || !sig_str) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Convert hex strings to bytes
unsigned char pubkey_bytes[32];
unsigned char sig_bytes[64];
if (nostr_hex_to_bytes(pubkey_str, pubkey_bytes, 32) != 0 ||
nostr_hex_to_bytes(sig_str, sig_bytes, 64) != 0) {
free(serialize_string);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Parse the public key into secp256k1 format
nostr_secp256k1_xonly_pubkey xonly_pubkey;
if (!nostr_secp256k1_xonly_pubkey_parse(&xonly_pubkey, pubkey_bytes)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Verify Schnorr signature
if (!nostr_secp256k1_schnorrsig_verify(sig_bytes, event_hash, &xonly_pubkey)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
free(serialize_string);
return NOSTR_SUCCESS;
}
/**
* Complete validation of a NOSTR event
* Performs both structure and cryptographic validation
*/
int nostr_validate_event(cJSON* event) {
// First validate structure (fast check)
int structure_result = nostr_validate_event_structure(event);
if (structure_result != NOSTR_SUCCESS) {
return structure_result;
}
// Then verify signature (expensive check)
return nostr_verify_event_signature(event);
}

View File

@@ -0,0 +1,23 @@
/*
* NOSTR Core Library - NIP-001: Basic Protocol Flow
*
* Event creation, signing, serialization and core protocol functions
*/
#ifndef NIP001_H
#define NIP001_H
#include <stdint.h>
#include <time.h>
#include "../cjson/cJSON.h"
// Function declarations
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
// Event validation functions
int nostr_validate_event_structure(cJSON* event);
int nostr_verify_event_signature(cJSON* event);
int nostr_validate_event(cJSON* event);
#endif // NIP001_H

View File

@@ -0,0 +1,104 @@
/*
* NOSTR Core Library - NIP-006: Key Derivation from Mnemonic
*/
#include "nip006.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include "../nostr_core/nostr_common.h"
#include "nostr_platform.h"
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) {
if (!private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (nostr_platform_random(private_key, 32) != 0) {
return NOSTR_ERROR_IO_FAILED;
}
// Validate private key
if (nostr_ec_private_key_verify(private_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Generate public key from private key (already x-only for NOSTR)
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key) {
if (!mnemonic || mnemonic_size < 256 || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate entropy for 12-word mnemonic
unsigned char entropy[16];
if (nostr_platform_random(entropy, sizeof(entropy)) != 0) {
return NOSTR_ERROR_IO_FAILED;
}
// Generate mnemonic from entropy
if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive keys from the generated mnemonic
return nostr_derive_keys_from_mnemonic(mnemonic, account, private_key, public_key);
}
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key) {
if (!mnemonic || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate mnemonic
if (nostr_bip39_mnemonic_validate(mnemonic) != 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Convert mnemonic to seed
unsigned char seed[64];
if (nostr_bip39_mnemonic_to_seed(mnemonic, "", seed, sizeof(seed)) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive master key from seed
nostr_hd_key_t master_key;
if (nostr_bip32_key_from_seed(seed, sizeof(seed), &master_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// NIP-06 path: m/44'/1237'/account'/0/0
nostr_hd_key_t derived_key;
uint32_t path[] = {
0x80000000 + 44, // 44' (hardened)
0x80000000 + 1237, // 1237' (hardened)
0x80000000 + account, // account' (hardened)
0, // 0 (not hardened)
0 // 0 (not hardened)
};
if (nostr_bip32_derive_path(&master_key, path, sizeof(path) / sizeof(path[0]), &derived_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Extract private key and public key
memcpy(private_key, derived_key.private_key, 32);
memcpy(public_key, derived_key.public_key + 1, 32); // Remove compression prefix for x-only
return NOSTR_SUCCESS;
}
// Note: nostr_detect_input_type, nostr_decode_nsec, and nostr_decode_npub
// are implemented in NIP-019 to avoid multiple definitions

View File

@@ -0,0 +1,30 @@
/*
* NOSTR Core Library - NIP-006: Key Derivation from Mnemonic
*/
#ifndef NIP006_H
#define NIP006_H
#include "nip001.h"
#include <stdint.h>
// Input type detection
typedef enum {
NOSTR_INPUT_UNKNOWN = 0,
NOSTR_INPUT_NSEC_HEX,
NOSTR_INPUT_NSEC_BECH32,
NOSTR_INPUT_MNEMONIC
} nostr_input_type_t;
// Function declarations
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key);
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key);
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key);
nostr_input_type_t nostr_detect_input_type(const char* input);
int nostr_decode_nsec(const char* input, unsigned char* private_key);
int nostr_decode_npub(const char* input, unsigned char* public_key);
#endif // NIP006_H

View File

@@ -0,0 +1,265 @@
/*
* NOSTR Core Library - NIP-019: Bech32-encoded Entities
*/
#include "nip019.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "../nostr_core/nostr_common.h"
#define BECH32_CONST 1
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
static const int8_t bech32_charset_rev[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
static uint32_t bech32_polymod_step(uint32_t pre) {
uint8_t b = pre >> 25;
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
(-((b >> 1) & 1) & 0x26508e6dUL) ^
(-((b >> 2) & 1) & 0x1ea119faUL) ^
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
(-((b >> 4) & 1) & 0x2a1462b3UL);
}
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
*outlen = 0;
while (inlen--) {
val = (val << inbits) | *(in++);
bits += inbits;
while (bits >= outbits) {
bits -= outbits;
out[(*outlen)++] = (val >> bits) & maxv;
}
}
if (pad) {
if (bits) {
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
}
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
return 0;
}
return 1;
}
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
uint32_t chk = 1;
size_t i, hrp_len = strlen(hrp);
for (i = 0; i < hrp_len; ++i) {
int ch = hrp[i];
if (ch < 33 || ch > 126) return 0;
if (ch >= 'A' && ch <= 'Z') return 0;
chk = bech32_polymod_step(chk) ^ (ch >> 5);
}
chk = bech32_polymod_step(chk);
for (i = 0; i < hrp_len; ++i) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
*(output++) = hrp[i];
}
*(output++) = '1';
for (i = 0; i < data_len; ++i) {
if (*data >> 5) return 0;
chk = bech32_polymod_step(chk) ^ (*data);
*(output++) = bech32_charset[*(data++)];
}
for (i = 0; i < 6; ++i) {
chk = bech32_polymod_step(chk);
}
chk ^= BECH32_CONST;
for (i = 0; i < 6; ++i) {
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
}
*output = 0;
return 1;
}
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
if (!input || !hrp || !data || !data_len) {
return 0;
}
size_t input_len = strlen(input);
size_t hrp_len = strlen(hrp);
if (input_len < hrp_len + 7) return 0;
if (strncmp(input, hrp, hrp_len) != 0) return 0;
if (input[hrp_len] != '1') return 0;
const char* data_part = input + hrp_len + 1;
size_t data_part_len = input_len - hrp_len - 1;
uint8_t values[256];
for (size_t i = 0; i < data_part_len; i++) {
unsigned char c = (unsigned char)data_part[i];
if (c >= 128) return 0;
int8_t val = bech32_charset_rev[c];
if (val == -1) return 0;
values[i] = (uint8_t)val;
}
if (data_part_len < 6) return 0;
uint32_t chk = 1;
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
}
chk = bech32_polymod_step(chk);
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
}
for (size_t i = 0; i < data_part_len; i++) {
chk = bech32_polymod_step(chk) ^ values[i];
}
if (chk != BECH32_CONST) return 0;
size_t payload_len = data_part_len - 6;
size_t decoded_len;
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
return 0;
}
*data_len = decoded_len;
return 1;
}
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output) {
if (!key || !hrp || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
uint8_t conv[64];
size_t conv_len;
if (!convert_bits(conv, &conv_len, 5, key, 32, 8, 1)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
if (!bech32_encode(output, hrp, conv, conv_len)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
nostr_input_type_t nostr_detect_input_type(const char* input) {
if (!input || strlen(input) == 0) {
return NOSTR_INPUT_UNKNOWN;
}
size_t len = strlen(input);
// Check for bech32 nsec
if (len > 5 && strncmp(input, "nsec1", 5) == 0) {
return NOSTR_INPUT_NSEC_BECH32;
}
// Check for hex nsec (64 characters, all hex)
if (len == 64) {
int is_hex = 1;
for (size_t i = 0; i < len; i++) {
if (!isxdigit((unsigned char)input[i])) {
is_hex = 0;
break;
}
}
if (is_hex) {
return NOSTR_INPUT_NSEC_HEX;
}
}
// Check for mnemonic (space-separated words)
int word_count = 0;
char temp[1024];
strncpy(temp, input, sizeof(temp) - 1);
temp[sizeof(temp) - 1] = '\0';
char* token = strtok(temp, " ");
while (token != NULL) {
word_count++;
token = strtok(NULL, " ");
}
// BIP39 mnemonics are typically 12, 18, or 24 words
if (word_count >= 12 && word_count <= 24) {
return NOSTR_INPUT_MNEMONIC;
}
return NOSTR_INPUT_UNKNOWN;
}
int nostr_decode_nsec(const char* input, unsigned char* private_key) {
if (!input || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) {
if (nostr_hex_to_bytes(input, private_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (type == NOSTR_INPUT_NSEC_BECH32) {
size_t decoded_len;
if (!bech32_decode(input, "nsec", private_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
// TODO: Add private key validation if crypto functions are available
return NOSTR_SUCCESS;
}
int nostr_decode_npub(const char* input, unsigned char* public_key) {
if (!input || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) { // Actually public key hex
if (nostr_hex_to_bytes(input, public_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (strncmp(input, "npub1", 4) == 0) { // Bech32 npub
size_t decoded_len;
if (!bech32_decode(input, "npub", public_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}

View File

@@ -0,0 +1,17 @@
/*
* NOSTR Core Library - NIP-019: Bech32-encoded Entities
*/
#ifndef NIP019_H
#define NIP019_H
#include "nip001.h"
#include "nip006.h" // For nostr_input_type_t enum
// Function declarations
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output);
nostr_input_type_t nostr_detect_input_type(const char* input);
int nostr_decode_nsec(const char* input, unsigned char* private_key);
int nostr_decode_npub(const char* input, unsigned char* public_key);
#endif // NIP019_H

View File

@@ -0,0 +1,106 @@
/*
* NOSTR Core Library - Common Utilities
*
* Common functions and utilities shared across the library
*/
#include "nostr_common.h"
#include "utils.h"
/**
* Convert error code to human-readable string
* Handles all error codes defined in nostr_common.h
*/
const char* nostr_strerror(int error_code) {
switch (error_code) {
case NOSTR_SUCCESS: return "Success";
case NOSTR_ERROR_INVALID_INPUT: return "Invalid input";
case NOSTR_ERROR_CRYPTO_FAILED: return "Cryptographic operation failed";
case NOSTR_ERROR_MEMORY_FAILED: return "Memory allocation failed";
case NOSTR_ERROR_IO_FAILED: return "I/O operation failed";
case NOSTR_ERROR_NETWORK_FAILED: return "Network operation failed";
case NOSTR_ERROR_NIP04_INVALID_FORMAT: return "NIP-04 invalid format";
case NOSTR_ERROR_NIP04_DECRYPT_FAILED: return "NIP-04 decryption failed";
case NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL: return "NIP-04 buffer too small";
case NOSTR_ERROR_NIP44_INVALID_FORMAT: return "NIP-44: Invalid format";
case NOSTR_ERROR_NIP44_DECRYPT_FAILED: return "NIP-44: Decryption failed";
case NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL: return "NIP-44: Buffer too small";
case NOSTR_ERROR_NIP05_INVALID_IDENTIFIER: return "NIP-05: Invalid identifier format";
case NOSTR_ERROR_NIP05_HTTP_FAILED: return "NIP-05: HTTP request failed";
case NOSTR_ERROR_NIP05_JSON_PARSE_FAILED: return "NIP-05: JSON parsing failed";
case NOSTR_ERROR_NIP05_NAME_NOT_FOUND: return "NIP-05: Name not found in .well-known";
case NOSTR_ERROR_NIP05_PUBKEY_MISMATCH: return "NIP-05: Public key mismatch";
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE: return "Event has invalid structure";
case NOSTR_ERROR_EVENT_INVALID_ID: return "Event has invalid ID";
case NOSTR_ERROR_EVENT_INVALID_PUBKEY: return "Event has invalid public key";
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: return "Event has invalid signature";
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT: return "Event has invalid timestamp";
case NOSTR_ERROR_EVENT_INVALID_KIND: return "Event has invalid kind";
case NOSTR_ERROR_EVENT_INVALID_TAGS: return "Event has invalid tags";
case NOSTR_ERROR_EVENT_INVALID_CONTENT: return "Event has invalid content";
case NOSTR_ERROR_NIP13_INSUFFICIENT: return "NIP-13: Insufficient PoW difficulty";
case NOSTR_ERROR_NIP13_NO_NONCE_TAG: return "NIP-13: Missing nonce tag";
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format";
case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch";
case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error";
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: return "NIP-42: Invalid challenge";
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: return "NIP-42: Challenge expired";
case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: return "NIP-42: Authentication event invalid";
case NOSTR_ERROR_NIP42_URL_MISMATCH: return "NIP-42: Relay URL mismatch";
case NOSTR_ERROR_NIP42_TIME_TOLERANCE: return "NIP-42: Timestamp outside tolerance";
case NOSTR_ERROR_NIP42_NOT_AUTHENTICATED: return "NIP-42: Client not authenticated";
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
case NOSTR_ERROR_NIP46_INVALID_BUNKER_URL: return "NIP-46: Invalid bunker URL";
case NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT: return "NIP-46: Invalid nostrconnect URL";
case NOSTR_ERROR_NIP46_INVALID_REQUEST: return "NIP-46: Invalid request payload";
case NOSTR_ERROR_NIP46_INVALID_RESPONSE: return "NIP-46: Invalid response payload";
case NOSTR_ERROR_NIP46_ENCRYPTION_FAILED: return "NIP-46: Encryption failed";
case NOSTR_ERROR_NIP46_DECRYPTION_FAILED: return "NIP-46: Decryption failed";
case NOSTR_ERROR_NIP46_CONNECTION_FAILED: return "NIP-46: Connection failed";
case NOSTR_ERROR_NIP46_TIMEOUT: return "NIP-46: Timeout";
case NOSTR_ERROR_NIP46_SECRET_MISMATCH: return "NIP-46: Secret mismatch";
case NOSTR_ERROR_NIP46_UNKNOWN_METHOD: return "NIP-46: Unknown method";
case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: return "NIP-46: Auth challenge required";
case NOSTR_ERROR_NIP46_NOT_CONNECTED: return "NIP-46: Not connected";
case NOSTR_ERROR_NIP60_INVALID_WALLET: return "NIP-60: Invalid wallet event";
case NOSTR_ERROR_NIP60_INVALID_TOKEN: return "NIP-60: Invalid token event";
case NOSTR_ERROR_NIP60_INVALID_HISTORY: return "NIP-60: Invalid history event";
case NOSTR_ERROR_NIP60_INVALID_QUOTE: return "NIP-60: Invalid quote event";
case NOSTR_ERROR_NIP60_DECRYPT_FAILED: return "NIP-60: Decryption failed";
case NOSTR_ERROR_NIP60_INVALID_PROOFS: return "NIP-60: Invalid proofs payload";
case NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS: return "NIP-60: Insufficient funds";
case NOSTR_ERROR_NIP61_INVALID_INFO: return "NIP-61: Invalid info event";
case NOSTR_ERROR_NIP61_INVALID_NUTZAP: return "NIP-61: Invalid nutzap event";
case NOSTR_ERROR_NIP61_MINT_MISMATCH: return "NIP-61: Mint mismatch";
case NOSTR_ERROR_NIP61_PUBKEY_MISMATCH: return "NIP-61: Pubkey mismatch";
case NOSTR_ERROR_NIP61_VERIFICATION_FAILED: return "NIP-61: Verification failed";
case NOSTR_ERROR_CASHU_HTTP_FAILED: return "Cashu: HTTP request failed";
case NOSTR_ERROR_CASHU_JSON_PARSE_FAILED: return "Cashu: JSON parsing failed";
case NOSTR_ERROR_CASHU_MINT_ERROR: return "Cashu: Mint returned an error";
case NOSTR_ERROR_CASHU_QUOTE_NOT_PAID: return "Cashu: Quote not paid";
case NOSTR_ERROR_CASHU_QUOTE_EXPIRED: return "Cashu: Quote expired";
case NOSTR_ERROR_CASHU_PROOFS_SPENT: return "Cashu: One or more proofs are already spent";
case NOSTR_ERROR_CASHU_CRYPTO_FAILED: return "Cashu: Cryptographic operation failed";
case NOSTR_ERROR_CASHU_INVALID_KEYSET: return "Cashu: Invalid keyset";
default: return "Unknown error";
}
}
/**
* Initialize the NOSTR library
*/
int nostr_init(void) {
if (nostr_crypto_init() != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
/**
* Cleanup the NOSTR library
*/
void nostr_cleanup(void) {
nostr_crypto_cleanup();
}

View File

@@ -0,0 +1,131 @@
/*
* NOSTR Core - Common Definitions
* Shared error constants and basic types for the modular NOSTR library
*/
#ifndef NOSTR_COMMON_H
#define NOSTR_COMMON_H
#include <stddef.h>
#include <stdint.h>
// Return codes
#define NOSTR_SUCCESS 0
#define NOSTR_ERROR_INVALID_INPUT -1
#define NOSTR_ERROR_CRYPTO_FAILED -2
#define NOSTR_ERROR_MEMORY_FAILED -3
#define NOSTR_ERROR_IO_FAILED -4
#define NOSTR_ERROR_NETWORK_FAILED -5
#define NOSTR_ERROR_NIP04_INVALID_FORMAT -10
#define NOSTR_ERROR_NIP04_DECRYPT_FAILED -11
#define NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL -12
#define NOSTR_ERROR_NIP44_INVALID_FORMAT -13
#define NOSTR_ERROR_NIP44_DECRYPT_FAILED -14
#define NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL -15
#define NOSTR_ERROR_NIP05_INVALID_IDENTIFIER -16
#define NOSTR_ERROR_NIP05_HTTP_FAILED -17
#define NOSTR_ERROR_NIP05_JSON_PARSE_FAILED -18
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
#define NOSTR_ERROR_EVENT_INVALID_ID -31
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
#define NOSTR_ERROR_EVENT_INVALID_CREATED_AT -34
#define NOSTR_ERROR_EVENT_INVALID_KIND -35
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
// Authentication Rules System Error Codes
#define NOSTR_ERROR_AUTH_RULES_DISABLED -50
#define NOSTR_ERROR_AUTH_RULES_DENIED -51
#define NOSTR_ERROR_AUTH_RULES_DB_FAILED -52
#define NOSTR_ERROR_AUTH_RULES_INVALID_RULE -53
#define NOSTR_ERROR_AUTH_RULES_CACHE_FAILED -54
#define NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND -55
// NIP-13 PoW-specific error codes
#define NOSTR_ERROR_NIP13_INSUFFICIENT -100
#define NOSTR_ERROR_NIP13_NO_NONCE_TAG -101
#define NOSTR_ERROR_NIP13_INVALID_NONCE_TAG -102
#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103
#define NOSTR_ERROR_NIP13_CALCULATION -104
// NIP-42 Authentication-specific error codes
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205
#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
// NIP-46 Remote Signing error codes
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
#define NOSTR_ERROR_NIP46_TIMEOUT -307
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
// NIP-60 Cashu Wallet error codes
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
// NIP-61 Nutzap error codes
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
// Cashu Mint client error codes
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
// Constants
#define NOSTR_PRIVATE_KEY_SIZE 32
#define NOSTR_PUBLIC_KEY_SIZE 32
#define NOSTR_HEX_KEY_SIZE 65 // 64 + null terminator
#define NOSTR_BECH32_KEY_SIZE 100
#define NOSTR_MAX_CONTENT_SIZE 2048
#define NOSTR_MAX_URL_SIZE 256
#define NIP05_DEFAULT_TIMEOUT 10
// NIP-04 Constants
#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
#define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
// NIP-44 Constants
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535 // 64KB - 1 (NIP-44 spec compliant)
// Forward declaration for cJSON (to avoid requiring cJSON.h in header)
struct cJSON;
// Function declarations
const char* nostr_strerror(int error_code);
// Library initialization functions
int nostr_init(void);
void nostr_cleanup(void);
#endif // NOSTR_COMMON_H

View File

@@ -0,0 +1,20 @@
#ifndef NOSTR_PLATFORM_H
#define NOSTR_PLATFORM_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Fill buffer with cryptographically secure random bytes.
* Returns 0 on success, -1 on failure.
*/
int nostr_platform_random(unsigned char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_PLATFORM_H */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
/*
* NOSTR Core Library - Utilities
*
* General utility functions used across multiple NIPs
*/
#ifndef NOSTR_UTILS_H
#define NOSTR_UTILS_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
// Convert bytes to hexadecimal string
void nostr_bytes_to_hex(const unsigned char *bytes, size_t len, char *hex);
// Convert hexadecimal string to bytes
int nostr_hex_to_bytes(const char *hex, unsigned char *bytes, size_t len);
// Base64 encoding function
size_t base64_encode(const unsigned char *data, size_t len, char *output,
size_t output_size);
// Base64 decoding function
size_t base64_decode(const char *input, unsigned char *output);
// =============================================================================
// CORE CRYPTO FUNCTIONS
// =============================================================================
// Initialize crypto subsystem
int nostr_crypto_init(void);
// Cleanup crypto subsystem
void nostr_crypto_cleanup(void);
// SHA-256 hash function
int nostr_sha256(const unsigned char *data, size_t len, unsigned char *hash);
// =============================================================================
// STREAMING SHA-256 FUNCTIONS
// =============================================================================
// SHA-256 streaming context
typedef struct {
uint32_t state[8]; // Current hash state
unsigned char buffer[64]; // Input buffer for incomplete blocks
uint64_t bitlen; // Total bits processed
size_t buflen; // Current buffer length
} nostr_sha256_ctx_t;
// Initialize SHA-256 streaming context
int nostr_sha256_init(nostr_sha256_ctx_t* ctx);
// Update SHA-256 context with new data
int nostr_sha256_update(nostr_sha256_ctx_t* ctx, const unsigned char* data, size_t len);
// Finalize SHA-256 and output hash
int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash);
// Stream SHA-256 hash of a file
int nostr_sha256_file_stream(const char* filename, unsigned char* hash);
// HMAC-SHA256
int nostr_hmac_sha256(const unsigned char *key, size_t key_len,
const unsigned char *data, size_t data_len,
unsigned char *output);
// HMAC-SHA512
int nostr_hmac_sha512(const unsigned char *key, size_t key_len,
const unsigned char *data, size_t data_len,
unsigned char *output);
// PBKDF2 with HMAC-SHA512
int nostr_pbkdf2_hmac_sha512(const unsigned char *password, size_t password_len,
const unsigned char *salt, size_t salt_len,
int iterations, unsigned char *output,
size_t output_len);
// SHA-512 implementation (for testing)
int nostr_sha512(const unsigned char *data, size_t len, unsigned char *hash);
// =============================================================================
// SECP256K1 ELLIPTIC CURVE FUNCTIONS
// =============================================================================
// Verify private key is valid
int nostr_ec_private_key_verify(const unsigned char *private_key);
// Generate public key from private key
int nostr_ec_public_key_from_private_key(const unsigned char *private_key,
unsigned char *public_key);
// Sign data with ECDSA
int nostr_ec_sign(const unsigned char *private_key, const unsigned char *hash,
unsigned char *signature);
// RFC 6979 deterministic nonce generation
int nostr_rfc6979_generate_k(const unsigned char *private_key,
const unsigned char *message_hash,
unsigned char *k_out);
int nostr_schnorr_sign(const unsigned char* private_key,
const unsigned char* hash,
unsigned char* signature);
// =============================================================================
// HKDF KEY DERIVATION FUNCTIONS
// =============================================================================
// HKDF Extract step
int nostr_hkdf_extract(const unsigned char *salt, size_t salt_len,
const unsigned char *ikm, size_t ikm_len,
unsigned char *prk);
// HKDF Expand step
int nostr_hkdf_expand(const unsigned char *prk, size_t prk_len,
const unsigned char *info, size_t info_len,
unsigned char *okm, size_t okm_len);
// HKDF (Extract + Expand)
int nostr_hkdf(const unsigned char *salt, size_t salt_len,
const unsigned char *ikm, size_t ikm_len,
const unsigned char *info, size_t info_len, unsigned char *okm,
size_t okm_len);
// ECDH shared secret computation (for debugging)
int ecdh_shared_secret(const unsigned char *private_key,
const unsigned char *public_key_x,
unsigned char *shared_secret);
// =============================================================================
// BIP39 MNEMONIC FUNCTIONS
// =============================================================================
// Generate mnemonic from entropy
int nostr_bip39_mnemonic_from_bytes(const unsigned char *entropy,
size_t entropy_len, char *mnemonic);
// Validate mnemonic
int nostr_bip39_mnemonic_validate(const char *mnemonic);
// Convert mnemonic to seed
int nostr_bip39_mnemonic_to_seed(const char *mnemonic, const char *passphrase,
unsigned char *seed, size_t seed_len);
// =============================================================================
// BIP32 HD WALLET FUNCTIONS
// =============================================================================
typedef struct {
unsigned char private_key[32];
unsigned char public_key[33];
unsigned char chain_code[32];
uint32_t depth;
uint32_t parent_fingerprint;
uint32_t child_number;
} nostr_hd_key_t;
// Create master key from seed
int nostr_bip32_key_from_seed(const unsigned char *seed, size_t seed_len,
nostr_hd_key_t *master_key);
// Derive child key from parent
int nostr_bip32_derive_child(const nostr_hd_key_t *parent_key,
uint32_t child_number, nostr_hd_key_t *child_key);
// Derive key from path
int nostr_bip32_derive_path(const nostr_hd_key_t *master_key,
const uint32_t *path, size_t path_len,
nostr_hd_key_t *derived_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_UTILS_H

View File

@@ -0,0 +1,79 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "../nostr_core/nostr_platform.h"
#if defined(ARDUINO)
#include <Arduino.h>
#endif
#if defined(__has_include)
# if __has_include("pico/rand.h")
# include "pico/rand.h"
# define NOSTR_HAVE_PICO_RAND 1
# endif
# if __has_include("hardware/adc.h")
# include "hardware/adc.h"
# define NOSTR_HAVE_PICO_ADC 1
# endif
#endif
static uint32_t xorshift32(uint32_t *state) {
uint32_t x = *state ? *state : 0xA5A5A5A5u;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*state = x;
return x;
}
int nostr_platform_random(unsigned char *buf, size_t len) {
if (!buf) {
return -1;
}
size_t i = 0;
#if defined(NOSTR_HAVE_PICO_RAND)
while (i + 4 <= len) {
uint32_t r = get_rand_32();
memcpy(buf + i, &r, 4);
i += 4;
}
if (i < len) {
uint32_t r = get_rand_32();
memcpy(buf + i, &r, len - i);
}
return 0;
#else
uint32_t seed = 0x13579BDFu;
#if defined(ARDUINO)
seed ^= (uint32_t)micros();
seed ^= ((uint32_t)millis() << 16);
seed ^= (uint32_t)(uintptr_t)&seed;
#endif
#if defined(NOSTR_HAVE_PICO_ADC)
adc_init();
adc_set_temp_sensor_enabled(true);
adc_select_input(4);
for (int k = 0; k < 16; ++k) {
seed ^= ((uint32_t)adc_read() << ((k & 3) * 8));
}
#endif
while (i < len) {
uint32_t r = xorshift32(&seed);
#if defined(ARDUINO)
r ^= (uint32_t)micros();
#endif
size_t take = (len - i >= 4) ? 4 : (len - i);
memcpy(buf + i, &r, take);
i += take;
}
return 0;
#endif
}

View File

@@ -0,0 +1,929 @@
#ifndef SECP256K1_H
#define SECP256K1_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
/** Unless explicitly stated all pointer arguments must not be NULL.
*
* The following rules specify the order of arguments in API calls:
*
* 1. Context pointers go first, followed by output arguments, combined
* output/input arguments, and finally input-only arguments.
* 2. Array lengths always immediately follow the argument whose length
* they describe, even if this violates rule 1.
* 3. Within the OUT/OUTIN/IN groups, pointers to data that is typically generated
* later go first. This means: signatures, public nonces, secret nonces,
* messages, public keys, secret keys, tweaks.
* 4. Arguments that are not data pointers go last, from more complex to less
* complex: function pointers, algorithm names, messages, void pointers,
* counts, flags, booleans.
* 5. Opaque data pointers follow the function pointer they are to be passed to.
*/
/** Opaque data structure that holds context information
*
* The primary purpose of context objects is to store randomization data for
* enhanced protection against side-channel leakage. This protection is only
* effective if the context is randomized after its creation. See
* secp256k1_context_create for creation of contexts and
* secp256k1_context_randomize for randomization.
*
* A secondary purpose of context objects is to store pointers to callback
* functions that the library will call when certain error states arise. See
* secp256k1_context_set_error_callback as well as
* secp256k1_context_set_illegal_callback for details. Future library versions
* may use context objects for additional purposes.
*
* A constructed context can safely be used from multiple threads
* simultaneously, but API calls that take a non-const pointer to a context
* need exclusive access to it. In particular this is the case for
* secp256k1_context_destroy, secp256k1_context_preallocated_destroy,
* and secp256k1_context_randomize.
*
* Regarding randomization, either do it once at creation time (in which case
* you do not need any locking for the other calls), or use a read-write lock.
*/
typedef struct secp256k1_context_struct secp256k1_context;
/** Opaque data structure that holds a parsed and valid public key.
*
* The exact representation of data inside is implementation defined and not
* guaranteed to be portable between different platforms or versions. It is
* however guaranteed to be 64 bytes in size, and can be safely copied/moved.
* If you need to convert to a format suitable for storage or transmission,
* use secp256k1_ec_pubkey_serialize and secp256k1_ec_pubkey_parse. To
* compare keys, use secp256k1_ec_pubkey_cmp.
*/
typedef struct secp256k1_pubkey {
unsigned char data[64];
} secp256k1_pubkey;
/** Opaque data structure that holds a parsed ECDSA signature.
*
* The exact representation of data inside is implementation defined and not
* guaranteed to be portable between different platforms or versions. It is
* however guaranteed to be 64 bytes in size, and can be safely copied/moved.
* If you need to convert to a format suitable for storage, transmission, or
* comparison, use the secp256k1_ecdsa_signature_serialize_* and
* secp256k1_ecdsa_signature_parse_* functions.
*/
typedef struct secp256k1_ecdsa_signature {
unsigned char data[64];
} secp256k1_ecdsa_signature;
/** A pointer to a function to deterministically generate a nonce.
*
* Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail.
* Out: nonce32: pointer to a 32-byte array to be filled by the function.
* In: msg32: the 32-byte message hash being verified (will not be NULL)
* key32: pointer to a 32-byte secret key (will not be NULL)
* algo16: pointer to a 16-byte array describing the signature
* algorithm (will be NULL for ECDSA for compatibility).
* data: Arbitrary data pointer that is passed through.
* attempt: how many iterations we have tried to find a nonce.
* This will almost always be 0, but different attempt values
* are required to result in a different nonce.
*
* Except for test cases, this function should compute some cryptographic hash of
* the message, the algorithm, the key and the attempt.
*/
typedef int (*secp256k1_nonce_function)(
unsigned char *nonce32,
const unsigned char *msg32,
const unsigned char *key32,
const unsigned char *algo16,
void *data,
unsigned int attempt
);
# if !defined(SECP256K1_GNUC_PREREQ)
# if defined(__GNUC__)&&defined(__GNUC_MINOR__)
# define SECP256K1_GNUC_PREREQ(_maj,_min) \
((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min))
# else
# define SECP256K1_GNUC_PREREQ(_maj,_min) 0
# endif
# endif
/* When this header is used at build-time the SECP256K1_BUILD define needs to be set
* to correctly setup export attributes and nullness checks. This is normally done
* by secp256k1.c but to guard against this header being included before secp256k1.c
* has had a chance to set the define (e.g. via test harnesses that just includes
* secp256k1.c) we set SECP256K1_NO_BUILD when this header is processed without the
* BUILD define so this condition can be caught.
*/
#ifndef SECP256K1_BUILD
# define SECP256K1_NO_BUILD
#endif
/* Symbol visibility. */
#if !defined(SECP256K1_API) && defined(SECP256K1_NO_API_VISIBILITY_ATTRIBUTES)
/* The user has requested that we don't specify visibility attributes in
* the public API.
*
* Since all our non-API declarations use the static qualifier, this means
* that the user can use -fvisibility=<value> to set the visibility of the
* API symbols. For instance, -fvisibility=hidden can be useful *even for
* the API symbols*, e.g., when building a static library which is linked
* into a shared library, and the latter should not re-export the
* libsecp256k1 API.
*
* While visibility is a concept that applies only to shared libraries,
* setting visibility will still make a difference when building a static
* library: the visibility settings will be stored in the static library,
* solely for the potential case that the static library will be linked into
* a shared library. In that case, the stored visibility settings will
* resurface and be honored for the shared library. */
# define SECP256K1_API extern
#endif
#if !defined(SECP256K1_API)
# if defined(SECP256K1_BUILD)
/* On Windows, assume a shared library only if explicitly requested.
* 1. If using Libtool, it defines DLL_EXPORT automatically.
* 2. In other cases, SECP256K1_DLL_EXPORT must be defined. */
# if defined(_WIN32) && (defined(SECP256K1_DLL_EXPORT) || defined(DLL_EXPORT))
/* GCC for Windows (e.g., MinGW) accepts the __declspec syntax for
* MSVC compatibility. A __declspec declaration implies (but is not
* exactly equivalent to) __attribute__ ((visibility("default"))),
* and so we actually want __declspec even on GCC, see "Microsoft
* Windows Function Attributes" in the GCC manual and the
* recommendations in https://gcc.gnu.org/wiki/Visibility . */
# define SECP256K1_API extern __declspec(dllexport)
/* Avoid __attribute__ ((visibility("default"))) on Windows to get rid
* of warnings when compiling with -flto due to a bug in GCC, see
* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116478 . */
# elif !defined(_WIN32) && defined (__GNUC__) && (__GNUC__ >= 4)
# define SECP256K1_API extern __attribute__ ((visibility("default")))
# else
# define SECP256K1_API extern
# endif
# else
/* On Windows, SECP256K1_STATIC must be defined when consuming
* libsecp256k1 as a static library. Note that SECP256K1_STATIC is a
* "consumer-only" macro, and it has no meaning when building
* libsecp256k1. */
# if defined(_WIN32) && !defined(SECP256K1_STATIC)
# define SECP256K1_API extern __declspec(dllimport)
# else
# define SECP256K1_API extern
# endif
# endif
#endif
/* Warning attributes
* NONNULL is not used if SECP256K1_BUILD is set to avoid the compiler optimizing out
* some paranoid null checks. */
# if defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4)
# define SECP256K1_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__))
# else
# define SECP256K1_WARN_UNUSED_RESULT
# endif
# if !defined(SECP256K1_BUILD) && defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4)
# define SECP256K1_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x)))
# else
# define SECP256K1_ARG_NONNULL(_x)
# endif
/* Attribute for marking functions, types, and variables as deprecated */
#if !defined(SECP256K1_BUILD) && defined(__has_attribute)
# if __has_attribute(__deprecated__)
# define SECP256K1_DEPRECATED(_msg) __attribute__ ((__deprecated__(_msg)))
# else
# define SECP256K1_DEPRECATED(_msg)
# endif
#else
# define SECP256K1_DEPRECATED(_msg)
#endif
/* All flags' lower 8 bits indicate what they're for. Do not use directly. */
#define SECP256K1_FLAGS_TYPE_MASK ((1 << 8) - 1)
#define SECP256K1_FLAGS_TYPE_CONTEXT (1 << 0)
#define SECP256K1_FLAGS_TYPE_COMPRESSION (1 << 1)
/* The higher bits contain the actual data. Do not use directly. */
#define SECP256K1_FLAGS_BIT_CONTEXT_VERIFY (1 << 8)
#define SECP256K1_FLAGS_BIT_CONTEXT_SIGN (1 << 9)
#define SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY (1 << 10)
#define SECP256K1_FLAGS_BIT_COMPRESSION (1 << 8)
/** Context flags to pass to secp256k1_context_create, secp256k1_context_preallocated_size, and
* secp256k1_context_preallocated_create. */
#define SECP256K1_CONTEXT_NONE (SECP256K1_FLAGS_TYPE_CONTEXT)
/** Deprecated context flags. These flags are treated equivalent to SECP256K1_CONTEXT_NONE. */
#define SECP256K1_CONTEXT_VERIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY)
#define SECP256K1_CONTEXT_SIGN (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN)
/* Testing flag. Do not use. */
#define SECP256K1_CONTEXT_DECLASSIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY)
/** Flag to pass to secp256k1_ec_pubkey_serialize. */
#define SECP256K1_EC_COMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION)
#define SECP256K1_EC_UNCOMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION)
/** Prefix byte used to tag various encoded curvepoints for specific purposes */
#define SECP256K1_TAG_PUBKEY_EVEN 0x02
#define SECP256K1_TAG_PUBKEY_ODD 0x03
#define SECP256K1_TAG_PUBKEY_UNCOMPRESSED 0x04
#define SECP256K1_TAG_PUBKEY_HYBRID_EVEN 0x06
#define SECP256K1_TAG_PUBKEY_HYBRID_ODD 0x07
/** A built-in constant secp256k1 context object with static storage duration, to be
* used in conjunction with secp256k1_selftest.
*
* This context object offers *only limited functionality* , i.e., it cannot be used
* for API functions that perform computations involving secret keys, e.g., signing
* and public key generation. If this restriction applies to a specific API function,
* it is mentioned in its documentation. See secp256k1_context_create if you need a
* full context object that supports all functionality offered by the library.
*
* It is highly recommended to call secp256k1_selftest before using this context.
*/
SECP256K1_API const secp256k1_context * const secp256k1_context_static;
/** Deprecated alias for secp256k1_context_static. */
SECP256K1_API const secp256k1_context * const secp256k1_context_no_precomp
SECP256K1_DEPRECATED("Use secp256k1_context_static instead");
/** Perform basic self tests (to be used in conjunction with secp256k1_context_static)
*
* This function performs self tests that detect some serious usage errors and
* similar conditions, e.g., when the library is compiled for the wrong endianness.
* This is a last resort measure to be used in production. The performed tests are
* very rudimentary and are not intended as a replacement for running the test
* binaries.
*
* It is highly recommended to call this before using secp256k1_context_static.
* It is not necessary to call this function before using a context created with
* secp256k1_context_create (or secp256k1_context_preallocated_create), which will
* take care of performing the self tests.
*
* If the tests fail, this function will call the default error callback to abort the
* program (see secp256k1_context_set_error_callback).
*/
SECP256K1_API void secp256k1_selftest(void);
/** Create a secp256k1 context object (in dynamically allocated memory).
*
* This function uses malloc to allocate memory. It is guaranteed that malloc is
* called at most once for every call of this function. If you need to avoid dynamic
* memory allocation entirely, see secp256k1_context_static and the functions in
* secp256k1_preallocated.h.
*
* Returns: pointer to a newly created context object.
* In: flags: Always set to SECP256K1_CONTEXT_NONE (see below).
*
* The only valid non-deprecated flag in recent library versions is
* SECP256K1_CONTEXT_NONE, which will create a context sufficient for all functionality
* offered by the library. All other (deprecated) flags will be treated as equivalent
* to the SECP256K1_CONTEXT_NONE flag. Though the flags parameter primarily exists for
* historical reasons, future versions of the library may introduce new flags.
*
* If the context is intended to be used for API functions that perform computations
* involving secret keys, e.g., signing and public key generation, then it is highly
* recommended to call secp256k1_context_randomize on the context before calling
* those API functions. This will provide enhanced protection against side-channel
* leakage, see secp256k1_context_randomize for details.
*
* Do not create a new context object for each operation, as construction and
* randomization can take non-negligible time.
*/
SECP256K1_API secp256k1_context *secp256k1_context_create(
unsigned int flags
) SECP256K1_WARN_UNUSED_RESULT;
/** Copy a secp256k1 context object (into dynamically allocated memory).
*
* This function uses malloc to allocate memory. It is guaranteed that malloc is
* called at most once for every call of this function. If you need to avoid dynamic
* memory allocation entirely, see the functions in secp256k1_preallocated.h.
*
* Cloning secp256k1_context_static is not possible, and should not be emulated by
* the caller (e.g., using memcpy). Create a new context instead.
*
* Returns: pointer to a newly created context object.
* Args: ctx: pointer to a context to copy (not secp256k1_context_static).
*/
SECP256K1_API secp256k1_context *secp256k1_context_clone(
const secp256k1_context *ctx
) SECP256K1_ARG_NONNULL(1) SECP256K1_WARN_UNUSED_RESULT;
/** Destroy a secp256k1 context object (created in dynamically allocated memory).
*
* The context pointer may not be used afterwards.
*
* The context to destroy must have been created using secp256k1_context_create
* or secp256k1_context_clone. If the context has instead been created using
* secp256k1_context_preallocated_create or secp256k1_context_preallocated_clone, the
* behaviour is undefined. In that case, secp256k1_context_preallocated_destroy must
* be used instead.
*
* Args: ctx: pointer to a context to destroy, constructed using
* secp256k1_context_create or secp256k1_context_clone
* (i.e., not secp256k1_context_static).
*/
SECP256K1_API void secp256k1_context_destroy(
secp256k1_context *ctx
) SECP256K1_ARG_NONNULL(1);
/** Set a callback function to be called when an illegal argument is passed to
* an API call. It will only trigger for violations that are mentioned
* explicitly in the header.
*
* The philosophy is that these shouldn't be dealt with through a specific
* return value, as calling code should not have branches to deal with the case
* that this code itself is broken.
*
* On the other hand, during debug stage, one would want to be informed about
* such mistakes, and the default (crashing) may be inadvisable. Should this
* callback return instead of crashing, the return value and output arguments
* of the API function call are undefined. Moreover, the same API call may
* trigger the callback again in this case.
*
* When this function has not been called (or called with fun==NULL), then the
* default callback will be used. The library provides a default callback which
* writes the message to stderr and calls abort. This default callback can be
* replaced at link time if the preprocessor macro
* USE_EXTERNAL_DEFAULT_CALLBACKS is defined, which is the case if the build
* has been configured with --enable-external-default-callbacks (GNU Autotools) or
* -DSECP256K1_USE_EXTERNAL_DEFAULT_CALLBACKS=ON (CMake). Then the
* following two symbols must be provided to link against:
* - void secp256k1_default_illegal_callback_fn(const char *message, void *data);
* - void secp256k1_default_error_callback_fn(const char *message, void *data);
* The library may call a default callback even before a proper callback data
* pointer could have been set using secp256k1_context_set_illegal_callback or
* secp256k1_context_set_error_callback, e.g., when the creation of a context
* fails. In this case, the corresponding default callback will be called with
* the data pointer argument set to NULL.
*
* Args: ctx: pointer to a context object.
* In: fun: pointer to a function to call when an illegal argument is
* passed to the API, taking a message and an opaque pointer.
* (NULL restores the default callback.)
* data: the opaque pointer to pass to fun above, must be NULL for the
* default callback.
*
* See also secp256k1_context_set_error_callback.
*/
SECP256K1_API void secp256k1_context_set_illegal_callback(
secp256k1_context *ctx,
void (*fun)(const char *message, void *data),
const void *data
) SECP256K1_ARG_NONNULL(1);
/** Set a callback function to be called when an internal consistency check
* fails.
*
* The default callback writes an error message to stderr and calls abort
* to abort the program.
*
* This can only trigger in case of a hardware failure, miscompilation,
* memory corruption, serious bug in the library, or other error that would
* result in undefined behaviour. It will not trigger due to mere
* incorrect usage of the API (see secp256k1_context_set_illegal_callback
* for that). After this callback returns, anything may happen, including
* crashing.
*
* Args: ctx: pointer to a context object.
* In: fun: pointer to a function to call when an internal error occurs,
* taking a message and an opaque pointer (NULL restores the
* default callback, see secp256k1_context_set_illegal_callback
* for details).
* data: the opaque pointer to pass to fun above, must be NULL for the
* default callback.
*
* See also secp256k1_context_set_illegal_callback.
*/
SECP256K1_API void secp256k1_context_set_error_callback(
secp256k1_context *ctx,
void (*fun)(const char *message, void *data),
const void *data
) SECP256K1_ARG_NONNULL(1);
/** A pointer to a function implementing SHA256's internal compression function.
*
* This function processes one or more contiguous 64-byte message blocks and
* updates the internal SHA256 state accordingly. The function is not responsible
* for counting consumed blocks or bytes, nor for performing padding.
*
* In/Out: state: pointer to eight 32-bit words representing the current internal state;
* the state is updated in place.
* In: blocks64: pointer to concatenation of n_blocks blocks, of 64 bytes each.
* no alignment guarantees are made for this pointer.
* n_blocks: number of contiguous 64-byte blocks to process.
*/
typedef void (*secp256k1_sha256_compression_function)(
uint32_t *state,
const unsigned char *blocks64,
size_t n_blocks
);
/**
* Set a callback function to override the internal SHA256 compression function.
*
* This installs a function to replace the built-in block-compression
* step used by the library's internal SHA256 implementation.
* The provided callback must exactly implement the effect of n_blocks
* repeated applications of the SHA256 compression function.
*
* This API exists to support environments that wish to route the
* SHA256 compression step through a hardware-accelerated or otherwise
* specialized implementation. It is NOT meant for replacing SHA256
* with a different hash function.
*
* Args: ctx: pointer to a context object.
* In: fn_compression: pointer to a function implementing the compression function;
* passing NULL restores the default implementation.
*/
SECP256K1_API void secp256k1_context_set_sha256_compression(
secp256k1_context *ctx,
secp256k1_sha256_compression_function fn_compression
) SECP256K1_ARG_NONNULL(1);
/** Parse a variable-length public key into the pubkey object.
*
* Returns: 1 if the public key was fully valid.
* 0 if the public key could not be parsed or is invalid.
* Args: ctx: pointer to a context object.
* Out: pubkey: pointer to a pubkey object. If 1 is returned, it is set to a
* parsed version of input. If not, its value is undefined.
* In: input: pointer to a serialized public key
* inputlen: length of the array pointed to by input
*
* This function supports parsing compressed (33 bytes, header byte 0x02 or
* 0x03), uncompressed (65 bytes, header byte 0x04), or hybrid (65 bytes, header
* byte 0x06 or 0x07) format public keys.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_parse(
const secp256k1_context *ctx,
secp256k1_pubkey *pubkey,
const unsigned char *input,
size_t inputlen
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Serialize a pubkey object into a serialized byte sequence.
*
* Returns: 1 always.
* Args: ctx: pointer to a context object.
* Out: output: pointer to a 65-byte (if compressed==0) or 33-byte (if
* compressed==1) byte array to place the serialized key
* in.
* In/Out: outputlen: pointer to an integer which is initially set to the
* size of output, and is overwritten with the written
* size.
* In: pubkey: pointer to a secp256k1_pubkey containing an
* initialized public key.
* flags: SECP256K1_EC_COMPRESSED if serialization should be in
* compressed format, otherwise SECP256K1_EC_UNCOMPRESSED.
*/
SECP256K1_API int secp256k1_ec_pubkey_serialize(
const secp256k1_context *ctx,
unsigned char *output,
size_t *outputlen,
const secp256k1_pubkey *pubkey,
unsigned int flags
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
/** Compare two public keys using lexicographic (of compressed serialization) order
*
* Returns: <0 if the first public key is less than the second
* >0 if the first public key is greater than the second
* 0 if the two public keys are equal
* Args: ctx: pointer to a context object
* In: pubkey1: first public key to compare
* pubkey2: second public key to compare
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_cmp(
const secp256k1_context *ctx,
const secp256k1_pubkey *pubkey1,
const secp256k1_pubkey *pubkey2
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Sort public keys using lexicographic (of compressed serialization) order
*
* Returns: 0 if the arguments are invalid. 1 otherwise.
*
* Args: ctx: pointer to a context object
* In: pubkeys: array of pointers to pubkeys to sort
* n_pubkeys: number of elements in the pubkeys array
*/
SECP256K1_API int secp256k1_ec_pubkey_sort(
const secp256k1_context *ctx,
const secp256k1_pubkey **pubkeys,
size_t n_pubkeys
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2);
/** Parse an ECDSA signature in compact (64 bytes) format.
*
* Returns: 1 when the signature could be parsed, 0 otherwise.
* Args: ctx: pointer to a context object
* Out: sig: pointer to a signature object
* In: input64: pointer to the 64-byte array to parse
*
* The signature must consist of a 32-byte big endian R value, followed by a
* 32-byte big endian S value. If R or S fall outside of [0..order-1], the
* encoding is invalid. R and S with value 0 are allowed in the encoding.
*
* After the call, sig will always be initialized. If parsing failed or R or
* S are zero, the resulting sig value is guaranteed to fail verification for
* any message and public key.
*/
SECP256K1_API int secp256k1_ecdsa_signature_parse_compact(
const secp256k1_context *ctx,
secp256k1_ecdsa_signature *sig,
const unsigned char *input64
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Parse a DER ECDSA signature.
*
* Returns: 1 when the signature could be parsed, 0 otherwise.
* Args: ctx: pointer to a context object
* Out: sig: pointer to a signature object
* In: input: pointer to the signature to be parsed
* inputlen: the length of the array pointed to be input
*
* This function will accept any valid DER encoded signature, even if the
* encoded numbers are out of range.
*
* After the call, sig will always be initialized. If parsing failed or the
* encoded numbers are out of range, signature verification with it is
* guaranteed to fail for every message and public key.
*/
SECP256K1_API int secp256k1_ecdsa_signature_parse_der(
const secp256k1_context *ctx,
secp256k1_ecdsa_signature *sig,
const unsigned char *input,
size_t inputlen
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Serialize an ECDSA signature in DER format.
*
* Returns: 1 if enough space was available to serialize, 0 otherwise
* Args: ctx: pointer to a context object
* Out: output: pointer to an array to store the DER serialization
* In/Out: outputlen: pointer to a length integer. Initially, this integer
* should be set to the length of output. After the call
* it will be set to the length of the serialization (even
* if 0 was returned).
* In: sig: pointer to an initialized signature object
*/
SECP256K1_API int secp256k1_ecdsa_signature_serialize_der(
const secp256k1_context *ctx,
unsigned char *output,
size_t *outputlen,
const secp256k1_ecdsa_signature *sig
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
/** Serialize an ECDSA signature in compact (64 byte) format.
*
* Returns: 1
* Args: ctx: pointer to a context object
* Out: output64: pointer to a 64-byte array to store the compact serialization
* In: sig: pointer to an initialized signature object
*
* See secp256k1_ecdsa_signature_parse_compact for details about the encoding.
*/
SECP256K1_API int secp256k1_ecdsa_signature_serialize_compact(
const secp256k1_context *ctx,
unsigned char *output64,
const secp256k1_ecdsa_signature *sig
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Verify an ECDSA signature.
*
* Returns: 1: correct signature
* 0: incorrect or unparseable signature
* Args: ctx: pointer to a context object
* In: sig: the signature being verified.
* msghash32: the 32-byte message hash being verified.
* The verifier must make sure to apply a cryptographic
* hash function to the message by itself and not accept an
* msghash32 value directly. Otherwise, it would be easy to
* create a "valid" signature without knowledge of the
* secret key. See also
* https://bitcoin.stackexchange.com/a/81116/35586 for more
* background on this topic.
* pubkey: pointer to an initialized public key to verify with.
*
* To avoid accepting malleable signatures, only ECDSA signatures in lower-S
* form are accepted.
*
* If you need to accept ECDSA signatures from sources that do not obey this
* rule, apply secp256k1_ecdsa_signature_normalize to the signature prior to
* verification, but be aware that doing so results in malleable signatures.
*
* For details, see the comments for that function.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ecdsa_verify(
const secp256k1_context *ctx,
const secp256k1_ecdsa_signature *sig,
const unsigned char *msghash32,
const secp256k1_pubkey *pubkey
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
/** Convert a signature to a normalized lower-S form.
*
* Returns: 1 if sigin was not normalized, 0 if it already was.
* Args: ctx: pointer to a context object
* Out: sigout: pointer to a signature to fill with the normalized form,
* or copy if the input was already normalized. (can be NULL if
* you're only interested in whether the input was already
* normalized).
* In: sigin: pointer to a signature to check/normalize (can be identical to sigout)
*
* With ECDSA a third-party can forge a second distinct signature of the same
* message, given a single initial signature, but without knowing the key. This
* is done by negating the S value modulo the order of the curve, 'flipping'
* the sign of the random point R which is not included in the signature.
*
* Forgery of the same message isn't universally problematic, but in systems
* where message malleability or uniqueness of signatures is important this can
* cause issues. This forgery can be blocked by all verifiers forcing signers
* to use a normalized form.
*
* The lower-S form reduces the size of signatures slightly on average when
* variable length encodings (such as DER) are used and is cheap to verify,
* making it a good choice. Security of always using lower-S is assured because
* anyone can trivially modify a signature after the fact to enforce this
* property anyway.
*
* The lower S value is always between 0x1 and
* 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
* inclusive.
*
* No other forms of ECDSA malleability are known and none seem likely, but
* there is no formal proof that ECDSA, even with this additional restriction,
* is free of other malleability. Commonly used serialization schemes will also
* accept various non-unique encodings, so care should be taken when this
* property is required for an application.
*
* The secp256k1_ecdsa_sign function will by default create signatures in the
* lower-S form, and secp256k1_ecdsa_verify will not accept others. In case
* signatures come from a system that cannot enforce this property,
* secp256k1_ecdsa_signature_normalize must be called before verification.
*/
SECP256K1_API int secp256k1_ecdsa_signature_normalize(
const secp256k1_context *ctx,
secp256k1_ecdsa_signature *sigout,
const secp256k1_ecdsa_signature *sigin
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(3);
/** An implementation of RFC6979 (using HMAC-SHA256) as nonce generation function.
* If a data pointer is passed, it is assumed to be a pointer to 32 bytes of
* extra entropy.
*/
SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_rfc6979;
/** A default safe nonce generation function (currently equal to secp256k1_nonce_function_rfc6979). */
SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_default;
/** Create an ECDSA signature.
*
* Returns: 1: signature created
* 0: the nonce generation function failed, or the secret key was invalid.
* Args: ctx: pointer to a context object (not secp256k1_context_static).
* Out: sig: pointer to an array where the signature will be placed.
* In: msghash32: the 32-byte message hash being signed.
* seckey: pointer to a 32-byte secret key.
* noncefp: pointer to a nonce generation function. If NULL,
* secp256k1_nonce_function_default is used.
* ndata: pointer to arbitrary data used by the nonce generation function
* (can be NULL). If it is non-NULL and
* secp256k1_nonce_function_default is used, then ndata must be a
* pointer to 32-bytes of additional data.
*
* The created signature is always in lower-S form. See
* secp256k1_ecdsa_signature_normalize for more details.
*/
SECP256K1_API int secp256k1_ecdsa_sign(
const secp256k1_context *ctx,
secp256k1_ecdsa_signature *sig,
const unsigned char *msghash32,
const unsigned char *seckey,
secp256k1_nonce_function noncefp,
const void *ndata
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
/** Verify an elliptic curve secret key.
*
* A secret key is valid if it is not 0 and less than the secp256k1 curve order
* when interpreted as an integer (most significant byte first). The
* probability of choosing a 32-byte string uniformly at random which is an
* invalid secret key is negligible. However, if it does happen it should
* be assumed that the randomness source is severely broken and there should
* be no retry.
*
* Returns: 1: secret key is valid
* 0: secret key is invalid
* Args: ctx: pointer to a context object.
* In: seckey: pointer to a 32-byte secret key.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_verify(
const secp256k1_context *ctx,
const unsigned char *seckey
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2);
/** Compute the public key for a secret key.
*
* Returns: 1: secret was valid, public key stores.
* 0: secret was invalid, try again.
* Args: ctx: pointer to a context object (not secp256k1_context_static).
* Out: pubkey: pointer to the created public key.
* In: seckey: pointer to a 32-byte secret key.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_create(
const secp256k1_context *ctx,
secp256k1_pubkey *pubkey,
const unsigned char *seckey
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Negates a secret key in place.
*
* Returns: 0 if the given secret key is invalid according to
* secp256k1_ec_seckey_verify. 1 otherwise
* Args: ctx: pointer to a context object
* In/Out: seckey: pointer to the 32-byte secret key to be negated. If the
* secret key is invalid according to
* secp256k1_ec_seckey_verify, this function returns 0 and
* seckey will be set to some unspecified value.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_negate(
const secp256k1_context *ctx,
unsigned char *seckey
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2);
/** Negates a public key in place.
*
* Returns: 1 always
* Args: ctx: pointer to a context object
* In/Out: pubkey: pointer to the public key to be negated.
*/
SECP256K1_API int secp256k1_ec_pubkey_negate(
const secp256k1_context *ctx,
secp256k1_pubkey *pubkey
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2);
/** Tweak a secret key by adding tweak to it.
*
* Returns: 0 if the arguments are invalid or the resulting secret key would be
* invalid (only when the tweak is the negation of the secret key). 1
* otherwise.
* Args: ctx: pointer to a context object.
* In/Out: seckey: pointer to a 32-byte secret key. If the secret key is
* invalid according to secp256k1_ec_seckey_verify, this
* function returns 0. seckey will be set to some unspecified
* value if this function returns 0.
* In: tweak32: pointer to a 32-byte tweak, which must be valid according to
* secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly
* random 32-byte tweaks, the chance of being invalid is
* negligible (around 1 in 2^128).
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_add(
const secp256k1_context *ctx,
unsigned char *seckey,
const unsigned char *tweak32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Tweak a public key by adding tweak times the generator to it.
*
* Returns: 0 if the arguments are invalid or the resulting public key would be
* invalid (only when the tweak is the negation of the corresponding
* secret key). 1 otherwise.
* Args: ctx: pointer to a context object.
* In/Out: pubkey: pointer to a public key object. pubkey will be set to an
* invalid value if this function returns 0.
* In: tweak32: pointer to a 32-byte tweak, which must be valid according to
* secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly
* random 32-byte tweaks, the chance of being invalid is
* negligible (around 1 in 2^128).
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_add(
const secp256k1_context *ctx,
secp256k1_pubkey *pubkey,
const unsigned char *tweak32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Tweak a secret key by multiplying it by a tweak.
*
* Returns: 0 if the arguments are invalid. 1 otherwise.
* Args: ctx: pointer to a context object.
* In/Out: seckey: pointer to a 32-byte secret key. If the secret key is
* invalid according to secp256k1_ec_seckey_verify, this
* function returns 0. seckey will be set to some unspecified
* value if this function returns 0.
* In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to
* secp256k1_ec_seckey_verify, this function returns 0. For
* uniformly random 32-byte arrays the chance of being invalid
* is negligible (around 1 in 2^128).
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_mul(
const secp256k1_context *ctx,
unsigned char *seckey,
const unsigned char *tweak32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Tweak a public key by multiplying it by a tweak value.
*
* Returns: 0 if the arguments are invalid. 1 otherwise.
* Args: ctx: pointer to a context object.
* In/Out: pubkey: pointer to a public key object. pubkey will be set to an
* invalid value if this function returns 0.
* In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to
* secp256k1_ec_seckey_verify, this function returns 0. For
* uniformly random 32-byte arrays the chance of being invalid
* is negligible (around 1 in 2^128).
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_mul(
const secp256k1_context *ctx,
secp256k1_pubkey *pubkey,
const unsigned char *tweak32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Randomizes the context to provide enhanced protection against side-channel leakage.
*
* Returns: 1: randomization successful
* 0: error
* Args: ctx: pointer to a context object (not secp256k1_context_static).
* In: seed32: pointer to a 32-byte random seed (NULL resets to initial state).
*
* While secp256k1 code is written and tested to be constant-time no matter what
* secret values are, it is possible that a compiler may output code which is not,
* and also that the CPU may not emit the same radio frequencies or draw the same
* amount of power for all values. Randomization of the context shields against
* side-channel observations which aim to exploit secret-dependent behaviour in
* certain computations which involve secret keys.
*
* It is highly recommended to call this function on contexts returned from
* secp256k1_context_create or secp256k1_context_clone (or from the corresponding
* functions in secp256k1_preallocated.h) before using these contexts to call API
* functions that perform computations involving secret keys, e.g., signing and
* public key generation. It is possible to call this function more than once on
* the same context, and doing so before every few computations involving secret
* keys is recommended as a defense-in-depth measure. Randomization of the static
* context secp256k1_context_static is not supported.
*
* Currently, the random seed is mainly used for blinding multiplications of a
* secret scalar with the elliptic curve base point. Multiplications of this
* kind are performed by exactly those API functions which are documented to
* require a context that is not secp256k1_context_static. As a rule of thumb,
* these are all functions which take a secret key (or a keypair) as an input.
* A notable exception to that rule is the ECDH module, which relies on a different
* kind of elliptic curve point multiplication and thus does not benefit from
* enhanced protection against side-channel leakage currently.
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_context_randomize(
secp256k1_context *ctx,
const unsigned char *seed32
) SECP256K1_ARG_NONNULL(1);
/** Add a number of public keys together.
*
* Returns: 1: the sum of the public keys is valid.
* 0: the sum of the public keys is not valid.
* Args: ctx: pointer to a context object.
* Out: out: pointer to a public key object for placing the resulting public key.
* In: ins: pointer to array of pointers to public keys.
* n: the number of public keys to add together (must be at least 1).
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_combine(
const secp256k1_context *ctx,
secp256k1_pubkey *out,
const secp256k1_pubkey * const *ins,
size_t n
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
/** Compute a tagged hash as defined in BIP-340.
*
* This is useful for creating a message hash and achieving domain separation
* through an application-specific tag. This function returns
* SHA256(SHA256(tag)||SHA256(tag)||msg). Therefore, tagged hash
* implementations optimized for a specific tag can precompute the SHA256 state
* after hashing the tag hashes.
*
* Returns: 1 always.
* Args: ctx: pointer to a context object
* Out: hash32: pointer to a 32-byte array to store the resulting hash
* In: tag: pointer to an array containing the tag
* taglen: length of the tag array
* msg: pointer to an array containing the message
* msglen: length of the message array
*/
SECP256K1_API int secp256k1_tagged_sha256(
const secp256k1_context *ctx,
unsigned char *hash32,
const unsigned char *tag,
size_t taglen,
const unsigned char *msg,
size_t msglen
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5);
#ifdef __cplusplus
}
#endif
#endif /* SECP256K1_H */

Some files were not shown because too many files have changed in this diff Show More