Compare commits

...

19 Commits

Author SHA1 Message Date
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
77 changed files with 22302 additions and 310 deletions

View File

@@ -56,6 +56,7 @@ RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev
# Copy source files
COPY src/ /build/src/
COPY resources/tui_continuous/ /build/resources/tui_continuous/
COPY resources/pqclean/ /build/resources/pqclean/
# Build nsigner as a fully static binary
RUN ARCH="$(uname -m)"; \
@@ -70,6 +71,11 @@ RUN ARCH="$(uname -m)"; \
-I/build/nostr_core_lib/nostr_core \
-I/build/nostr_core_lib/cjson \
-I/build/resources/tui_continuous \
-I/build/resources/pqclean \
-I/build/resources/pqclean/common \
-I/build/resources/pqclean/crypto_sign/ml-dsa-65 \
-I/build/resources/pqclean/crypto_sign/slh-dsa-128s \
-I/build/resources/pqclean/crypto_kem/ml-kem-768 \
/build/src/main.c \
/build/src/secure_mem.c \
/build/src/mnemonic.c \
@@ -83,6 +89,30 @@ RUN ARCH="$(uname -m)"; \
/build/src/key_store.c \
/build/src/socket_name.c \
/build/src/auth_envelope.c \
/build/src/miner.c \
/build/src/pq_crypto.c \
/build/src/pq_drbg.c \
/build/resources/pqclean/crypto_sign/ml-dsa-65/sign.c \
/build/resources/pqclean/crypto_sign/ml-dsa-65/poly.c \
/build/resources/pqclean/crypto_sign/ml-dsa-65/ntt.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/sign.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/hash.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/thash.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/utils.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/wots.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/fors.c \
/build/resources/pqclean/crypto_sign/slh-dsa-128s/address.c \
/build/resources/pqclean/common/fips202.c \
/build/resources/pqclean/common/sha2.c \
/build/resources/pqclean/common/crypto_backend_openssl.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/reduce.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/ntt.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/cbd.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/verify.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/symmetric.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/poly.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/indcpa.c \
/build/resources/pqclean/crypto_kem/ml-kem-768/kem.c \
/build/resources/tui_continuous/tui_continuous.c \
"$NOSTR_LIB" \
-o /build/nsigner_static \

136
Makefile
View File

@@ -1,5 +1,5 @@
CC := gcc
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -DNOSTR_ENABLE_NSIGNER_CLIENT=1 -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 -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson -Iresources/tui_continuous -Iresources/pqclean -Iresources/pqclean/crypto_sign/ml-dsa-65 -Iresources/pqclean/crypto_sign/slh-dsa-128s -Iresources/pqclean/crypto_kem/ml-kem-768 -Iresources/pqclean/common
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
SRC_DIR := src
@@ -10,6 +10,31 @@ EXAMPLES_DIR := examples
TARGET_DEV := $(BUILD_DIR)/nsigner
# PQClean ML-DSA-65 sources (Phase 3)
PQCLEAN_DIR := resources/pqclean
PQCLEAN_SOURCES := \
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/sign.c \
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/poly.c \
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/ntt.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/sign.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/hash.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/thash.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/utils.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/wots.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/fors.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/address.c \
$(PQCLEAN_DIR)/common/fips202.c \
$(PQCLEAN_DIR)/common/sha2.c \
$(PQCLEAN_DIR)/common/crypto_backend_openssl.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/reduce.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/ntt.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/cbd.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/verify.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/symmetric.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/poly.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/indcpa.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/kem.c
SOURCES := \
$(SRC_DIR)/main.c \
$(SRC_DIR)/secure_mem.c \
@@ -24,6 +49,10 @@ SOURCES := \
$(SRC_DIR)/key_store.c \
$(SRC_DIR)/socket_name.c \
$(SRC_DIR)/auth_envelope.c \
$(SRC_DIR)/miner.c \
$(SRC_DIR)/pq_crypto.c \
$(SRC_DIR)/pq_drbg.c \
$(PQCLEAN_SOURCES) \
resources/tui_continuous/tui_continuous.c
HEADERS :=
@@ -40,16 +69,29 @@ TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
TEST_MINE_EVENT_TARGET := $(BUILD_DIR)/test_mine_event
TEST_PQ_CRYPTO_TARGET := $(BUILD_DIR)/test_pq_crypto
TEST_ED25519_X25519_TARGET := $(BUILD_DIR)/test_ed25519_x25519
TEST_ML_DSA_65_TARGET := $(BUILD_DIR)/test_ml_dsa_65
TEST_SLH_DSA_128S_TARGET := $(BUILD_DIR)/test_slh_dsa_128s
TEST_ML_KEM_768_TARGET := $(BUILD_DIR)/test_ml_kem_768
TEST_PUBKEY_FORMAT_TARGET := $(BUILD_DIR)/test_pubkey_format
TEST_ALGORITHM_API_TARGET := $(BUILD_DIR)/test_algorithm_api
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
EXAMPLE_PQ_SIGN_TARGET := $(BUILD_DIR)/example_pq_sign
EXAMPLE_PQ_KEM_TARGET := $(BUILD_DIR)/example_pq_kem
EXAMPLE_SSH_SIGN_TARGET := $(BUILD_DIR)/example_ssh_sign
DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99
.PHONY: all lib dev static static-debug static-arm64 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
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)
@@ -72,7 +114,7 @@ static-arm64:
firmware-feather:
cd firmware/feather_s3_tft && idf.py build
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-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_TARGET)
@@ -107,9 +149,33 @@ test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
./$(TEST_QREXEC_AUTH_TARGET)
test-mine-event: $(TEST_MINE_EVENT_TARGET) $(TARGET_DEV)
./$(TEST_MINE_EVENT_TARGET)
test-pq-crypto: $(TEST_PQ_CRYPTO_TARGET)
./$(TEST_PQ_CRYPTO_TARGET)
test-ed25519-x25519: $(TEST_ED25519_X25519_TARGET)
./$(TEST_ED25519_X25519_TARGET)
test-ml-dsa-65: $(TEST_ML_DSA_65_TARGET)
./$(TEST_ML_DSA_65_TARGET)
test-slh-dsa-128s: $(TEST_SLH_DSA_128S_TARGET)
./$(TEST_SLH_DSA_128S_TARGET)
test-ml-kem-768: $(TEST_ML_KEM_768_TARGET)
./$(TEST_ML_KEM_768_TARGET)
test-pubkey-format: $(TEST_PUBKEY_FORMAT_TARGET)
./$(TEST_PUBKEY_FORMAT_TARGET)
test-algorithm-api: $(TEST_ALGORITHM_API_TARGET)
./$(TEST_ALGORITHM_API_TARGET)
test-client: examples
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET)
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(EXAMPLE_PQ_SIGN_TARGET) $(EXAMPLE_PQ_KEM_TARGET) $(EXAMPLE_SSH_SIGN_TARGET) $(DEMO_C99_TARGET)
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
@@ -127,13 +193,13 @@ $(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c -o $(TEST_SELECTOR_TARGET) $(LDFLAGS)
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
@mkdir -p $(BUILD_DIR)
@@ -155,6 +221,38 @@ $(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envel
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
$(TEST_MINE_EVENT_TARGET): $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
$(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c -o $(TEST_PQ_CRYPTO_TARGET) $(LDFLAGS)
$(TEST_ED25519_X25519_TARGET): $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -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 -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 -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 -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 -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
@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 -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)
@@ -167,5 +265,25 @@ $(EXAMPLE_GET_PUBKEY_TCP_TARGET): $(EXAMPLES_DIR)/get_pubkey_tcp.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_tcp.c -o $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(LDFLAGS)
$(EXAMPLE_GET_PUBKEY_QREXEC_TARGET): $(EXAMPLES_DIR)/get_pubkey_qrexec.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_qrexec.c -o $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(LDFLAGS)
$(EXAMPLE_PQ_SIGN_TARGET): $(EXAMPLES_DIR)/pq_sign_example.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/pq_sign_example.c -o $(EXAMPLE_PQ_SIGN_TARGET) $(LDFLAGS)
$(EXAMPLE_PQ_KEM_TARGET): $(EXAMPLES_DIR)/pq_kem_example.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/pq_kem_example.c -o $(EXAMPLE_PQ_KEM_TARGET) $(LDFLAGS)
$(EXAMPLE_SSH_SIGN_TARGET): $(EXAMPLES_DIR)/ssh_sign_example.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/ssh_sign_example.c -o $(EXAMPLE_SSH_SIGN_TARGET) $(LDFLAGS)
$(DEMO_C99_TARGET): $(CLIENT_DIR)/demo_c99.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(CLIENT_DIR)/demo_c99.c -o $(DEMO_C99_TARGET) $(LDFLAGS)
clean:
rm -rf $(BUILD_DIR)

244
README.md
View File

@@ -169,6 +169,145 @@ A single memorized mnemonic can deterministically recover multiple key domains t
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.
## 4b. Crypto palette
`n_signer` supports six cryptographic algorithms, all derived deterministically from the same BIP-39 mnemonic. Each algorithm is bound to a specific `(purpose, curve)` pair and a distinct BIP-44 derivation path.
### 4b.1 Algorithms
| Algorithm | Curve label | Purpose | FIPS standard | Key sizes (priv / pub) | Derivation path |
|---|---|---|---|---|---|
| secp256k1 | `secp256k1` | `nostr` | — | 32 / 32 bytes | `m/44'/1237'/<n>'/0/0` (NIP-06) |
| ed25519 | `ed25519` | `ssh` | — | 32 / 32 bytes | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) |
| x25519 | `x25519` | `age` | — | 32 / 32 bytes | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) |
| ML-DSA-65 | `ml-dsa-65` | `pq-sig` | FIPS 204 | 4032 / 1952 bytes | `m/44'/102003'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
| SLH-DSA-128s | `slh-dsa-128s` | `pq-sig` | FIPS 205 | 64 / 32 bytes | `m/44'/102004'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
| ML-KEM-768 | `ml-kem-768` | `pq-kem` | FIPS 203 | 2400 / 1184 bytes | `m/44'/102005'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
All six algorithms are always compiled in on every target (host x86_64 static binary and ESP32 firmware). The PQ implementations are vendored from [PQClean](https://github.com/PQClean/PQClean) (public domain / CC0).
### 4b.2 Key derivation
All keys derive deterministically from the BIP-39 mnemonic:
- **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. This produces deterministic PQ key pairs from the mnemonic — same mnemonic, same role, same key pair every time. See [`documents/SECURITY.md`](documents/SECURITY.md) §17 for the security argument.
The new algorithms use BIP-44 coin types `102001``102005` (unregistered in SLIP-44, chosen to avoid collisions with real cryptocurrencies). All non-secp256k1 paths are fully hardened per SLIP-0010.
### 4b.3 Post-quantum context
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. ML-KEM-768 protects key agreement against this threat. ML-DSA-65 and SLH-DSA-128s protect signatures against future quantum forgery.
All three are FIPS-standardized (FIPS 203, 204, 205) and are provided as **additional options**, not replacements for secp256k1. Nostr continues to use secp256k1 exclusively. PQ algorithms are opt-in per role.
Note: OpenSSH does not yet support PQ signing keys. The `pq-sig` purpose is forward-looking — the primitives are ready for when the ecosystem adopts them.
### 4b.4 Structured `get_public_key` response
For secp256k1, `get_public_key` returns the plain 64-hex-char public key string (backward compatible). An optional `{"format": "structured"}` in the options object requests a structured JSON response.
For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s, ML-KEM-768), `get_public_key` always returns a structured JSON object (serialized as a string):
```json
{"algorithm": "ml-dsa-65", "public_key": "<hex>", "key_id": "<16 hex chars>"}
```
The `key_id` is a short display identifier (first 16 hex chars of the public key). PQ public keys are large (ML-DSA-65: 3904 hex chars; ML-KEM-768: 2368 hex chars).
## 4c. Algorithm-based API
In addition to the role-based API, n_signer supports an **algorithm-based API** where the caller specifies the algorithm and derivation index directly, without needing to know role names. This is the preferred API for new clients.
### 4c.1 New verbs
| Verb | Description | Algorithm parameter | Key parameter |
|---|---|---|---|
| `sign` | Sign arbitrary bytes | `algorithm` | `index` |
| `verify` | Verify a signature | `algorithm` | `index` |
| `encapsulate` | KEM encapsulation | `algorithm` | `public_key` (peer's) |
| `decapsulate` | KEM decapsulation | `algorithm` | `index` |
| `derive_shared_secret` | ECDH key agreement (x25519) | `algorithm` | `index` + `peer_public_key` |
| `get_public_key` | Get public key (algorithm-based) | `algorithm` | `index` |
### 4c.2 Algorithm names
| String | Algorithm | Key type |
|---|---|---|
| `secp256k1` | secp256k1 (Schnorr/ECDSA) | Signature |
| `ed25519` | ed25519 | Signature |
| `ml-dsa-65` | ML-DSA-65 (FIPS 204) | Signature |
| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205) | Signature |
| `x25519` | X25519 (ECDH) | Key agreement |
| `ml-kem-768` | ML-KEM-768 (FIPS 203) | KEM |
### 4c.3 Request examples
**Sign with ed25519:**
```json
{"id":"1","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}
```
Response: `{"id":"1","result":{"signature":"<hex>","algorithm":"ed25519","key_id":"<16hex>"}}`
**Sign with secp256k1 (ECDSA scheme):**
```json
{"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"secp256k1","index":0,"scheme":"ecdsa"}]}
```
The `scheme` parameter is optional for secp256k1: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
**Get public key (algorithm-based):**
```json
{"id":"3","method":"get_public_key","params":[{"algorithm":"ml-dsa-65","index":0}]}
```
Response: `{"id":"3","result":{"algorithm":"ml-dsa-65","public_key":"<hex>","key_id":"<16hex>"}}`
**KEM encapsulate:**
```json
{"id":"4","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]}
```
**KEM decapsulate:**
```json
{"id":"5","method":"decapsulate","params":["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]}
```
**ECDH shared secret (x25519):**
```json
{"id":"6","method":"derive_shared_secret","params":["<peer_pubkey_hex>",{"algorithm":"x25519","index":0}]}
```
### 4c.4 Old verb aliases
Old verb names map to the new algorithm-based verbs when used with the `algorithm` parameter:
| Old verb | New verb | Default algorithm |
|---|---|---|
| `sign_data` | `sign` | (from `algorithm` parameter) |
| `ssh_sign` | `sign` | `ed25519` |
| `verify_signature` | `verify` | (from `algorithm` parameter) |
| `kem_encapsulate` | `encapsulate` | `ml-kem-768` |
| `kem_decapsulate` | `decapsulate` | `ml-kem-768` |
Without the `algorithm` parameter, old verbs fall through to the role-based path (backward compatible).
### 4c.5 Algorithm-based preapprove
```
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
```
### 4c.6 Enforcement matrix
| Verb | Valid algorithms |
|---|---|
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
| `encapsulate` / `decapsulate` | ml-kem-768 |
| `derive_shared_secret` | x25519 |
| `get_public_key` | all algorithms |
| `sign_event` / `nip44_*` / `nip04_*` / `mine_event` | secp256k1 (Nostr protocol) |
## 5. Wire contract (JSON-RPC)
Request shape is JSON-RPC with NIP-46-style methods and optional trailing selector options.
@@ -178,14 +317,76 @@ Request shape is JSON-RPC with NIP-46-style methods and optional trailing select
{ "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" }] }
{ "id": "5", "method": "mine_event", "params": ["<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 }] }
```
Implemented signer verbs in this build:
**Nostr verbs** (require `purpose="nostr"`, `curve="secp256k1"`):
- `get_public_key`
- `sign_event`
- `nip04_encrypt` / `nip04_decrypt`
- `nip44_encrypt` / `nip44_decrypt`
- `mine_event` — add NIP-13 proof-of-work and sign (see below)
**Multi-algorithm verbs**:
| Verb | Required purpose | Required curve | Description |
|---|---|---|---|
| `get_public_key` | any | any | Returns the role's public key. secp256k1 returns plain hex; other algorithms return structured JSON (see [§4b.4](#4b4-structured-get_public_key-response)). |
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | Sign arbitrary bytes (not a Nostr event). Returns `{"signature":"<hex>","algorithm":"<alg>"}`. |
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | Verify a signature. Returns `{"valid":true/false}`. |
| `ssh_sign` | `ssh` | `ed25519` | Sign an SSH authentication challenge in ed25519 format. Returns `{"signature":"<hex>","algorithm":"ed25519"}`. |
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` | Encapsulate against a peer's ML-KEM-768 public key. Returns `{"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}`. |
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` | Decapsulate a ciphertext using the role's ML-KEM-768 private key. Returns `{"shared_secret":"<hex>","algorithm":"ml-kem-768"}`. |
Example `sign_data` request:
```jsonc
{ "id": "6", "method": "sign_data", "params": ["<message_hex>", { "role": "pq-ml-dsa" }] }
```
Example `kem_encapsulate` request:
```jsonc
{ "id": "7", "method": "kem_encapsulate", "params": ["<peer_pubkey_hex>", { "role": "pq-kem-main" }] }
```
### `mine_event` — NIP-13 Proof-of-Work
Mines proof-of-work (adds a `nonce` tag per NIP-13) and signs the event in one step. The mining runs in a detached thread so the server stays responsive.
**Parameters (in options object):**
| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `difficulty` | One of difficulty/timeout | 0 (no target) | Target leading zero bits. Stops early if reached. |
| `timeout_sec` | One of difficulty/timeout | 600 (safety) | Time budget in seconds. Always returns best result found. |
| `threads` | No | 1 | Number of mining threads (max 32). |
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when either condition is met. The response always includes the best event found — timeout is not an error.
**Response format:**
```json
{
"id": "5",
"result": {
"event": "<signed event JSON with nonce tag>",
"achieved_difficulty": 18,
"target_difficulty": 20,
"target_reached": false,
"elapsed_sec": 30,
"attempts": 4523456
}
}
```
**Error codes:**
- `1007``no_termination_condition` (neither difficulty nor timeout_sec specified)
- `1008``mining_failed` (internal error)
Selector resolution order:
@@ -212,12 +413,52 @@ Representative error codes:
Selector resolution chooses *which* role. Enforcement decides *whether the requested method is valid* for that role.
### 6.1 Purpose values
| Purpose | Description |
|---|---|
| `nostr` | Nostr identities (secp256k1, NIP-06) |
| `bitcoin` | Bitcoin key trees (secp256k1, BIP-44) |
| `ssh` | SSH signing keys (ed25519) |
| `age` | age-style encryption identities (x25519) |
| `fips` | FIPS mesh/service identities |
| `pq-sig` | Post-quantum signatures (ML-DSA-65, SLH-DSA-128s) |
| `pq-kem` | Post-quantum key encapsulation (ML-KEM-768) |
### 6.2 Curve values
| Curve | Algorithms |
|---|---|
| `secp256k1` | ECDSA/Schnorr for Nostr, Bitcoin |
| `ed25519` | Ed25519 for SSH signatures |
| `x25519` | X25519 for key agreement (age) |
| `ml-dsa-65` | ML-DSA-65 (FIPS 204, lattice-based PQ signatures) |
| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205, hash-based PQ signatures) |
| `ml-kem-768` | ML-KEM-768 (FIPS 203, lattice-based PQ KEM) |
### 6.3 Enforcement matrix
| Verb | Required purpose | Required curve |
|---|---|---|
| `sign_event` | `nostr` | `secp256k1` |
| `mine_event` | `nostr` | `secp256k1` |
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` |
| `get_public_key` | any | any (must match role's declared curve) |
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `ssh_sign` | `ssh` | `ed25519` |
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` |
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` |
Example:
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
- `mine_event` requires `purpose="nostr"` and `curve="secp256k1"` (same as `sign_event`).
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
- If caller selects a `pq-sig`/`ml-dsa-65` key for `sign_event`, request fails with `purpose_mismatch`.
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
This prevents cross-protocol misuse inside one mnemonic-rooted signer process. Fail-closed: any unlisted `(verb, purpose, curve)` combination is rejected.
## 7. Transport
@@ -471,6 +712,7 @@ These items are designed and worth doing, but are not in the current implementat
- [`examples/get_pubkey_tcp.c`](examples/get_pubkey_tcp.c): C caller example using TCP transport with auth envelope
- [`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
- [`plans/post_quantum_crypto.md`](plans/post_quantum_crypto.md): post-quantum and multi-algorithm crypto expansion plan (all 8 phases)
- [`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`

View File

@@ -36,6 +36,79 @@ for the full integration contract.
The `nsigner ... client '<json>'` subcommand in [`src/main.c`](../src/main.c) is
unaffected — it has its own raw framing pass-through and never used this directory.
## Multi-Algorithm and Post-Quantum Verbs
n_signer supports six algorithms: `secp256k1` (Nostr), `ed25519` (SSH),
`x25519` (age/ECDH), `ml-dsa-65` (PQ signatures, FIPS 204), `slh-dsa-128s`
(PQ hash-based signatures, FIPS 205), and `ml-kem-768` (PQ KEM, FIPS 203).
Use `nsigner_client_call(client, "<verb>", params, &result)` with these verbs:
| Verb | Algorithms | Description |
|---|---|---|
| `get_public_key` | all | Returns the role's public key (see format below) |
| `sign_event` | secp256k1 | Sign a Nostr event (existing) |
| `nip44_encrypt` / `nip44_decrypt` | secp256k1 | NIP-44 (existing) |
| `nip04_encrypt` / `nip04_decrypt` | secp256k1 | NIP-04 (existing) |
| `mine_event` | secp256k1 | NIP-13 PoW mining + sign (existing) |
| `sign_data` | ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) |
| `verify_signature` | ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature against the role's pubkey |
| `ssh_sign` | ed25519 | Sign an SSH authentication challenge |
| `kem_encapsulate` | ml-kem-768 | Encapsulate with a peer's ML-KEM public key |
| `kem_decapsulate` | ml-kem-768 | Decapsulate a ciphertext with the role's ML-KEM private key |
### Algorithm-based API (new)
In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly, without needing a role:
| Verb | Algorithms | Description |
|---|---|---|
| `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 |
| `get_public_key` (with `algorithm`) | all | Get public key for a derived key |
Example: `nsigner_client_call(client, "sign", "[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]", &result)`
For secp256k1, the optional `scheme` parameter selects `"schnorr"` (default) or `"ecdsa"`.
Old verbs (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) also accept the `algorithm` parameter and map to the new verbs. Without `algorithm`, they use the role-based path (backward compatible).
### `get_public_key` response format
- **secp256k1 (backward compatible):** `result` is a plain hex string
(`cJSON_IsString(result)` is true, 64 hex chars).
- **secp256k1 with `{"format":"structured"}` option:** `result` is a JSON
string containing `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
- **All other algorithms:** `result` is always a JSON string containing
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`.
Clients should parse the `result` string with `cJSON_Parse` to extract the
`algorithm`, `public_key`, and `key_id` fields for non-secp256k1 algorithms.
### 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)

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. sign_event — sign a Nostr event (kind 1 text note)
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
*
* Note: mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
* See demo_javascript.js and demo_python.py for mine_event usage examples.
* The high-level nostr_signer API does not yet wrap 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: 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("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: nip44_encrypt / 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("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("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. sign_event — sign a Nostr event (kind 1 text note)
* 3. 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: 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: "sign_event",
params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }],
});
if (response.error) {
throw new Error(`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: nip44_encrypt / nip44_decrypt ===");
console.log(` plaintext: "${plaintext}"`);
console.log(` peer pubkey: ${pubkeyHex} (self)`);
// Encrypt
const encResponse = await callNsigner(targetQube, {
id: "3",
method: "nip44_encrypt",
params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }],
});
if (encResponse.error) {
throw new Error(`nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
}
const ciphertext = encResponse.result;
console.log(` ciphertext: ${ciphertext}`);
// Decrypt
const decResponse = await callNsigner(targetQube, {
id: "4",
method: "nip44_decrypt",
params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }],
});
if (decResponse.error) {
throw new Error(`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: 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: "mine_event",
params: [JSON.stringify(event), {
difficulty: 4,
threads: 4,
timeout_sec: 30,
nostr_index: nostrIndex,
}],
});
if (response.error) {
throw new Error(`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. sign_event — sign a Nostr event (kind 1 text note)
3. 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: 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": "sign_event",
"params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}],
},
)
if "error" in response:
raise RuntimeError(f"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: nip44_encrypt / nip44_decrypt ===")
print(f' plaintext: "{plaintext}"')
print(f" peer pubkey: {pubkey_hex} (self)")
# Encrypt
enc_response = call_nsigner(
target_qube,
{
"id": "3",
"method": "nip44_encrypt",
"params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}],
},
)
if "error" in enc_response:
raise RuntimeError(f"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": "nip44_decrypt",
"params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}],
},
)
if "error" in dec_response:
raise RuntimeError(f"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_mine_event(target_qube, nostr_index):
print("\n--- Demo 4: 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": "mine_event",
"params": [json.dumps(event), {
"difficulty": 4,
"threads": 4,
"timeout_sec": 30,
"nostr_index": nostr_index,
}],
},
)
if "error" in response:
raise RuntimeError(f"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_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

@@ -127,6 +127,25 @@ Methods are NIP-46 style verbs.
- `nip44_encrypt`
- `nip44_decrypt`
### 4.2b Algorithm-based verbs (new)
In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly:
- `sign` — sign arbitrary bytes (params: `[message_hex, {algorithm, index, scheme?}]`)
- `verify` — verify a signature (params: `[message_hex, signature_hex, {algorithm, index, scheme?}]`)
- `encapsulate` — KEM encapsulation (params: `[peer_pubkey_hex, {algorithm}]`)
- `decapsulate` — KEM decapsulation (params: `[ciphertext_hex, {algorithm, index}]`)
- `derive_shared_secret` — ECDH key agreement (params: `[peer_pubkey_hex, {algorithm, index}]`)
- `get_public_key` with `algorithm` parameter — returns structured JSON
Algorithm names: `secp256k1`, `ed25519`, `ml-dsa-65`, `slh-dsa-128s`, `x25519`, `ml-kem-768`
For secp256k1 `sign`/`verify`, the optional `scheme` parameter selects `"schnorr"` (default, BIP-340) or `"ecdsa"`.
Old verb aliases (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) map to the new verbs when used with the `algorithm` parameter. Without `algorithm`, they fall through to the role-based path.
See [README.md §4c](../README.md) for full details.
### 4.3 Selector options
The last param may include selector options:
@@ -484,7 +503,228 @@ Decrypt response:
---
## 11. Compatibility notes
## 11. Post-Quantum and Multi-Algorithm Support
n_signer supports six cryptographic algorithms, all derived deterministically
from the same BIP-39 mnemonic via distinct derivation paths:
| Algorithm | Purpose | Curve string | Purpose string | Derivation path |
|---|---|---|---|---|
| `secp256k1` | Nostr (sign_event, NIP-04/44) | `secp256k1` | `nostr` | `m/44'/1237'/<n>'/0/0` (NIP-06) |
| `ed25519` | SSH signing, general signatures | `ed25519` | `ssh` | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) |
| `x25519` | Key agreement (age, ECDH) | `x25519` | `age` | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) |
| `ml-dsa-65` | Post-quantum signatures (FIPS 204) | `ml-dsa-65` | `pq-sig` | `m/44'/102003'/<n>'/0'/0'` → seed → PQClean keygen |
| `slh-dsa-128s` | Post-quantum hash-based signatures (FIPS 205) | `slh-dsa-128s` | `pq-sig` | `m/44'/102004'/<n>'/0'/0'` → seed → PQClean keygen |
| `ml-kem-768` | Post-quantum key encapsulation (FIPS 203) | `ml-kem-768` | `pq-kem` | `m/44'/102005'/<n>'/0'/0'` → seed → PQClean keygen |
The `102XXX` coin types are unregistered in SLIP-44 and reserved by n_signer
for PQ/SSH/age algorithm families. All non-secp256k1 paths use SLIP-0010
all-hardened derivation.
### 11.1 Algorithm key sizes
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|---|---|---|---|---|---|
| secp256k1 | 32 bytes | 32 bytes | 64 bytes | — | — |
| ed25519 | 32 bytes | 32 bytes | 64 bytes | — | — |
| x25519 | 32 bytes | 32 bytes | — | — | 32 bytes |
| ML-DSA-65 | 1952 bytes | 4032 bytes | 3309 bytes | — | — |
| SLH-DSA-128s | 32 bytes | 64 bytes | 7856 bytes | — | — |
| ML-KEM-768 | 1184 bytes | 2400 bytes | — | 1088 bytes | 32 bytes |
PQ public keys and signatures are much larger than classical ones. Clients
must allocate buffers accordingly (ML-DSA-65 pubkey hex = 3904 chars;
SLH-DSA-128s signature hex = 15712 chars; ML-KEM-768 pubkey hex = 2368 chars).
### 11.2 New verbs
| Verb | Purpose | Allowed (purpose, curve) | Description |
|---|---|---|---|
| `sign_data` | pq-sig, ssh | (pq-sig, ml-dsa-65), (pq-sig, slh-dsa-128s), (ssh, ed25519) | Sign arbitrary bytes (not a Nostr event) |
| `verify_signature` | pq-sig, ssh | same as `sign_data` | Verify a signature against the role's public key |
| `ssh_sign` | ssh | (ssh, ed25519) | Sign an SSH authentication challenge (ed25519) |
| `kem_encapsulate` | pq-kem | (pq-kem, ml-kem-768) | Encapsulate: generate ciphertext + shared secret from a peer's ML-KEM public key |
| `kem_decapsulate` | pq-kem | (pq-kem, ml-kem-768) | Decapsulate: recover shared secret from ciphertext using the role's ML-KEM private key |
The existing Nostr verbs (`sign_event`, `nip44_*`, `nip04_*`, `mine_event`)
remain restricted to `purpose=nostr + curve=secp256k1`.
### 11.3 Structured `get_public_key` response format
`get_public_key` is a universal verb — it works for all six algorithms.
**For secp256k1 (backward compatibility):** the result is a plain hex string
(the existing format). Existing Nostr clients are unaffected.
```json
{ "id": "1", "result": "<64-char hex pubkey>" }
```
**For secp256k1 with `format: "structured"` option:** new clients can request
the structured format for consistency:
Request:
```json
{
"id": "1",
"method": "get_public_key",
"params": [{ "role": "main", "format": "structured" }]
}
```
Response:
```json
{
"id": "1",
"result": "{\"algorithm\":\"secp256k1\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}"
}
```
**For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s,
ML-KEM-768):** the result is always a structured JSON object serialized as a
string:
```json
{
"id": "1",
"result": {
"algorithm": "ml-dsa-65",
"public_key": "<hex-encoded public key>",
"key_id": "<first 16 hex chars of public key>"
}
}
```
The `key_id` is the first 16 hex characters of the public key — a short
display identifier similar to an SSH key fingerprint. The `result` field is a
JSON string (the object serialized), so clients must parse it twice: once for
the JSON-RPC envelope, once for the result object.
### 11.4 Example: `sign_data` (ML-DSA-65)
Request:
```json
{
"id": "10",
"method": "sign_data",
"params": ["68656c6c6f", { "role": "pq_sig" }]
}
```
Response:
```json
{
"id": "10",
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ml-dsa-65\"}"
}
```
The first param is the message bytes as hex. The signature is hex-encoded
(3309 bytes = 6618 hex chars for ML-DSA-65).
### 11.5 Example: `verify_signature` (ed25519)
Request:
```json
{
"id": "11",
"method": "verify_signature",
"params": ["<msg_hex>", "<sig_hex>", { "role": "ssh_main" }]
}
```
Response:
```json
{ "id": "11", "result": "{\"valid\":true}" }
```
The signature is verified against the role's derived public key.
### 11.6 Example: `ssh_sign` (ed25519)
Request:
```json
{
"id": "12",
"method": "ssh_sign",
"params": ["<session_id_hex>", { "role": "ssh_main" }]
}
```
Response:
```json
{
"id": "12",
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\"}"
}
```
The first param is the SSH session ID (or challenge) as hex. The signature is
a raw ed25519 signature (64 bytes = 128 hex chars).
### 11.7 Example: `kem_encapsulate` (ML-KEM-768)
Request:
```json
{
"id": "13",
"method": "kem_encapsulate",
"params": ["<peer_pubkey_hex>", { "role": "kem_main" }]
}
```
Response:
```json
{
"id": "13",
"result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
}
```
The first param is the peer's ML-KEM-768 public key as hex (1184 bytes = 2368
hex chars). The response contains the ciphertext (1088 bytes = 2176 hex chars)
and the shared secret (32 bytes = 64 hex chars). The encapsulating party keeps
the shared secret; the ciphertext is sent to the decapsulating party.
### 11.8 Example: `kem_decapsulate` (ML-KEM-768)
Request:
```json
{
"id": "14",
"method": "kem_decapsulate",
"params": ["<ciphertext_hex>", { "role": "kem_main" }]
}
```
Response:
```json
{
"id": "14",
"result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
}
```
The first param is the ciphertext from `kem_encapsulate` (1088 bytes = 2176
hex chars). The decapsulated shared secret will match the encapsulating
party's shared secret.
### 11.9 Example clients
See the `examples/` directory for working C clients demonstrating the new
verbs:
- [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) — ML-DSA-65
`get_public_key` + `sign_data`
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768
`get_public_key` + `kem_encapsulate` + `kem_decapsulate` (verifies shared
secrets match)
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519
`get_public_key` + `ssh_sign`
---
## 12. Compatibility notes
- If you are writing an autonomous agent client, pin to explicit socket name and explicit role selector.
- Keep method support feature-detected (`method_not_found` fallback).

View File

@@ -215,12 +215,18 @@ The signer enforces a strict `(verb, purpose, curve)` matrix:
| Verb | Required purpose | Required curve |
|---|---|---|
| `sign_event` | `nostr` | `secp256k1` |
| `get_public_key` | `nostr` | `secp256k1` |
| `mine_event` | `nostr` | `secp256k1` |
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` |
| `get_public_key` | any | any (must match role's declared curve) |
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `ssh_sign` | `ssh` | `ed25519` |
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` |
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` |
| Any other verb | rejected | rejected |
A pre-approval to use a Bitcoin-purposed key for `sign_event` does **not** override the enforcement matrix. The approval grants access to the key; enforcement still gates the verb. **Fail-closed**: unknown verbs 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.
@@ -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.
- [`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/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.
- [`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,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;
}

273
examples/pq_kem_example.c Normal file
View File

@@ -0,0 +1,273 @@
/*
* 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, "role", role);
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, "role", role);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "kem_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, "role", role);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "kem_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;
}

215
examples/pq_sign_example.c Normal file
View File

@@ -0,0 +1,215 @@
/*
* 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, "role", role);
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, "role", role);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "sign_data", 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;
}

212
examples/ssh_sign_example.c Normal file
View File

@@ -0,0 +1,212 @@
/*
* 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, "role", role);
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, "role", role);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "ssh_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

@@ -99,3 +99,165 @@ Notes:
- Typical working range is ~4.7uF to 22uF; 10uF is recommended
- Keep leads short for best stability
- Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually
## Post-quantum crypto support (Phase 7)
Both firmware targets (`feather_s3_tft` and `cyd_esp32_2432s028`) now include
the three NIST-standardized post-quantum algorithms alongside the existing
secp256k1 (Nostr) and new ed25519/x25519 classical algorithms:
| Algorithm | Standard | Purpose | Pub key | Priv key | Sig/Ct |
|---|---|---|---|---|---|
| secp256k1 | — | Nostr (existing) | 32 B | 32 B | 64 B |
| ed25519 | RFC 8032 | SSH signatures | 32 B | 32 B | 64 B |
| x25519 | RFC 7748 | Key agreement (age) | 32 B | 32 B | — |
| ML-DSA-65 | FIPS 204 | PQ signatures | 1952 B | 4032 B | 3309 B |
| SLH-DSA-128s | FIPS 205 | PQ hash-based sigs | 32 B | 64 B | 7856 B |
| ML-KEM-768 | FIPS 203 | PQ key encapsulation | 1184 B | 2400 B | 1088 B |
### mbedtls backend (vs OpenSSL on host)
The host build uses OpenSSL EVP for SHA-256, SHA-512, SHA3-256, SHA3-512,
SHAKE-128, and SHAKE-256. On ESP32, OpenSSL is not available. Instead, the
firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypto_backend.h`](../resources/pqclean/common/crypto_backend.h))
with two implementations:
- [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP)
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls)
The mbedtls backend uses:
- `mbedtls_sha256_ret()` for SHA-256 (ESP32-S3 hardware accelerated)
- `mbedtls_sha512_ret()` for SHA-512 (ESP32-S3 hardware accelerated)
- `mbedtls_md` API for SHA3-256, SHA3-512, SHAKE-128, SHAKE-256
### Enabling SHA3/SHAKE in menuconfig
mbedtls does not enable SHA3/SHAKE by default. You must enable them in
menuconfig before building:
```
Component config → mbedTLS → Hash functions → SHA-3
Component config → mbedTLS → Hash functions → SHAKE
```
Or add to `sdkconfig.defaults`:
```
CONFIG_MBEDTLS_SHA3_C=y
CONFIG_MBEDTLS_SHAKE_C=y
```
If mbedtls does not have SHAKE support, the firmware build will fail at link
time with unresolved `mbedtls_md_info_from_type(MBEDTLS_MD_SHAKE128)`. In that
case, either enable the config options above or patch mbedtls with a
Keccak/SHAKE contribution.
### 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,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

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

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h"
#include "mbedtls/ed25519.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u
typedef struct {
@@ -253,3 +261,316 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx);
return 0;
}
/* ====================================================================
* Phase 7: ed25519, x25519, and post-quantum key derivation
* ==================================================================== */
/* SLIP-0010 all-hardened derivation for ed25519/x25519.
*
* SLIP-0010 uses HMAC-SHA512 with a "ed25519 seed" or curve-specific key
* for the master key, and all derivation steps are hardened (the parent
* private key is prepended to the index data).
*
* For ed25519/x25519, the derived 512-bit HMAC output is split:
* - first 32 bytes = private key (the scalar)
* - last 32 bytes = chain code
*
* The private key IS the ed25519/x25519 secret — no tweak-add is needed
* (unlike secp256k1 BIP-32 where the child priv = parent_priv + HMAC).
*/
/* SLIP-0010 master key from seed: HMAC-SHA512(key="ed25519 seed", data=seed) */
static int slip10_master_from_seed(const uint8_t seed[64],
uint8_t priv[32], uint8_t chain[32]) {
static const uint8_t kEd25519Seed[] = "ed25519 seed";
uint8_t i64[64] = {0};
if (hmac_sha512(kEd25519Seed, sizeof(kEd25519Seed) - 1,
seed, 64, i64) != 0) {
return -1;
}
memcpy(priv, i64, 32);
memcpy(chain, i64 + 32, 32);
memset(i64, 0, sizeof(i64));
return 0;
}
/* SLIP-0010 hardened child derivation:
* HMAC-SHA512(key=chain, data=0x00 || priv || index_be32) */
static int slip10_ckd_priv(const uint8_t parent_priv[32],
const uint8_t parent_chain[32],
uint32_t index,
uint8_t child_priv[32],
uint8_t child_chain[32]) {
uint8_t data[37];
uint8_t i64[64] = {0};
/* Hardened derivation: 0x00 || priv || index (big-endian) */
data[0] = 0x00;
memcpy(data + 1, parent_priv, 32);
data[33] = (uint8_t)((index >> 24) & 0xFF);
data[34] = (uint8_t)((index >> 16) & 0xFF);
data[35] = (uint8_t)((index >> 8) & 0xFF);
data[36] = (uint8_t)(index & 0xFF);
if (hmac_sha512(parent_chain, 32, data, sizeof(data), i64) != 0) {
memset(data, 0, sizeof(data));
return -1;
}
memcpy(child_priv, i64, 32);
memcpy(child_chain, i64 + 32, 32);
memset(data, 0, sizeof(data));
memset(i64, 0, sizeof(i64));
return 0;
}
/* Derive a 32-byte seed via SLIP-0010 all-hardened path.
* path[] is an array of hardened indices (the caller sets the hardened flag).
* Returns the final 32-byte private material in `out_seed`. */
static int slip10_derive_seed(const uint8_t seed[64],
const uint32_t *path, size_t path_len,
uint8_t out_seed[32]) {
uint8_t priv[32], chain[32], next_priv[32], next_chain[32];
size_t i;
if (slip10_master_from_seed(seed, priv, chain) != 0) {
return -1;
}
for (i = 0; i < path_len; i++) {
if (slip10_ckd_priv(priv, chain, path[i],
next_priv, next_chain) != 0) {
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
return -1;
}
memcpy(priv, next_priv, 32);
memcpy(chain, next_chain, 32);
}
memcpy(out_seed, priv, 32);
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
memset(next_priv, 0, sizeof(next_priv));
memset(next_chain, 0, sizeof(next_chain));
return 0;
}
/* --- ed25519 --- */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102001'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102001u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key.
* 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
#include <stddef.h>
#include <stdint.h>
/* --- secp256k1 (Nostr, existing) --- */
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
/* --- ed25519 (SSH signatures) --- */
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
* privkey: 32-byte ed25519 private scalar
* pubkey: 32-byte ed25519 public key
* Returns 0 on success, -1 on error. */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* Signs a 32-byte message digest with ed25519.
* 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,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"
"mnemonic.c"
"key_derivation.c"
"pq_crypto_firmware.c"
"bech32.c"
"usb_transport.c"
"buttons.c"
@@ -23,6 +24,7 @@ idf_component_register(
REQUIRES
mbedtls
secp256k1
pqclean
json
espressif__esp_tinyusb
espressif__tinyusb

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h"
#include "mbedtls/ed25519.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u
typedef struct {
@@ -253,3 +261,316 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx);
return 0;
}
/* ====================================================================
* Phase 7: ed25519, x25519, and post-quantum key derivation
* ==================================================================== */
/* SLIP-0010 all-hardened derivation for ed25519/x25519.
*
* SLIP-0010 uses HMAC-SHA512 with a "ed25519 seed" or curve-specific key
* for the master key, and all derivation steps are hardened (the parent
* private key is prepended to the index data).
*
* For ed25519/x25519, the derived 512-bit HMAC output is split:
* - first 32 bytes = private key (the scalar)
* - last 32 bytes = chain code
*
* The private key IS the ed25519/x25519 secret — no tweak-add is needed
* (unlike secp256k1 BIP-32 where the child priv = parent_priv + HMAC).
*/
/* SLIP-0010 master key from seed: HMAC-SHA512(key="ed25519 seed", data=seed) */
static int slip10_master_from_seed(const uint8_t seed[64],
uint8_t priv[32], uint8_t chain[32]) {
static const uint8_t kEd25519Seed[] = "ed25519 seed";
uint8_t i64[64] = {0};
if (hmac_sha512(kEd25519Seed, sizeof(kEd25519Seed) - 1,
seed, 64, i64) != 0) {
return -1;
}
memcpy(priv, i64, 32);
memcpy(chain, i64 + 32, 32);
memset(i64, 0, sizeof(i64));
return 0;
}
/* SLIP-0010 hardened child derivation:
* HMAC-SHA512(key=chain, data=0x00 || priv || index_be32) */
static int slip10_ckd_priv(const uint8_t parent_priv[32],
const uint8_t parent_chain[32],
uint32_t index,
uint8_t child_priv[32],
uint8_t child_chain[32]) {
uint8_t data[37];
uint8_t i64[64] = {0};
/* Hardened derivation: 0x00 || priv || index (big-endian) */
data[0] = 0x00;
memcpy(data + 1, parent_priv, 32);
data[33] = (uint8_t)((index >> 24) & 0xFF);
data[34] = (uint8_t)((index >> 16) & 0xFF);
data[35] = (uint8_t)((index >> 8) & 0xFF);
data[36] = (uint8_t)(index & 0xFF);
if (hmac_sha512(parent_chain, 32, data, sizeof(data), i64) != 0) {
memset(data, 0, sizeof(data));
return -1;
}
memcpy(child_priv, i64, 32);
memcpy(child_chain, i64 + 32, 32);
memset(data, 0, sizeof(data));
memset(i64, 0, sizeof(i64));
return 0;
}
/* Derive a 32-byte seed via SLIP-0010 all-hardened path.
* path[] is an array of hardened indices (the caller sets the hardened flag).
* Returns the final 32-byte private material in `out_seed`. */
static int slip10_derive_seed(const uint8_t seed[64],
const uint32_t *path, size_t path_len,
uint8_t out_seed[32]) {
uint8_t priv[32], chain[32], next_priv[32], next_chain[32];
size_t i;
if (slip10_master_from_seed(seed, priv, chain) != 0) {
return -1;
}
for (i = 0; i < path_len; i++) {
if (slip10_ckd_priv(priv, chain, path[i],
next_priv, next_chain) != 0) {
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
return -1;
}
memcpy(priv, next_priv, 32);
memcpy(chain, next_chain, 32);
}
memcpy(out_seed, priv, 32);
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
memset(next_priv, 0, sizeof(next_priv));
memset(next_chain, 0, sizeof(next_chain));
return 0;
}
/* --- ed25519 --- */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102001'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102001u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key.
* 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
#include <stddef.h>
#include <stdint.h>
/* --- secp256k1 (Nostr, existing) --- */
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
/* --- ed25519 (SSH signatures) --- */
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
* privkey: 32-byte ed25519 private scalar
* pubkey: 32-byte ed25519 public key
* Returns 0 on success, -1 on error. */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* Signs a 32-byte message digest with ed25519.
* 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

@@ -181,10 +181,12 @@ build_release_binary() {
# Prevent stale artifacts from previous builds being uploaded.
rm -f build/nsigner_static_x86_64 build/nsigner_static_arm64
./build_static.sh > /dev/null 2>&1 || return 1
print_status "Building x86_64 static binary (this may take a few minutes with PQ algorithms)..."
./build_static.sh 2>&1 | tail -5 || return 1
verify_binary_version "build/nsigner_static_x86_64" "$NEW_VERSION" || return 1
./build_static.sh --arch arm64 > /dev/null 2>&1 || print_warning "ARM64 build failed (continuing)"
print_status "Building ARM64 static binary..."
./build_static.sh --arch arm64 2>&1 | tail -5 || print_warning "ARM64 build failed (continuing)"
if [[ -x "build/nsigner_static_arm64" ]]; then
verify_binary_version "build/nsigner_static_arm64" "$NEW_VERSION" || return 1
fi
@@ -221,11 +223,25 @@ create_gitea_release() {
response=$(curl -s -X POST "$api_url/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
-d "{\"tag_name\": \"$NEW_VERSION\", \"target_commitish\": \"master\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\", \"draft\": false, \"prerelease\": false}")
if echo "$response" | grep -q '"id"'; then
echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2
local release_id
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
# Work around Gitea bug: releases created via API get created_unix=0
# (epoch zero) in the database, causing them to not appear in the
# releases list or web UI. Fix the timestamp directly in the DB.
local now_unix
now_unix=$(date +%s)
print_status "Fixing release timestamp in Gitea database (workaround for Gitea bug)..."
ssh -o ConnectTimeout=10 ubuntu@laantungir.net \
"sudo sqlite3 /data/gitea/gitea.db \"UPDATE release SET created_unix=$now_unix WHERE id=$release_id;\"" \
2>/dev/null || print_warning "Could not fix release timestamp via SSH (release may not appear in web UI)"
echo "$release_id"
else
print_error "Failed to create Gitea release: $response"
return 1
fi
}

View File

@@ -84,12 +84,21 @@ resolve_nsigner_version() {
headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}")
fi
latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases" \
| jq -r '.[0].tag_name // empty' || true)"
# Use the git tags API and sort by version number, instead of the releases
# API which sorts by created_at timestamp. Gitea has a bug where some
# releases get epoch-zero timestamps and don't appear in the releases list.
latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/tags?limit=50" \
| jq -r '[.[].name] | map(select(startswith("v"))) | sort_by(ltrimstr("v") | split(".") | map(tonumber)) | last // empty' || true)"
if [[ -z "${latest_tag}" ]]; then
# Fallback: try the releases API (old behavior)
latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases" \
| jq -r '.[0].tag_name // empty' || true)"
fi
if [[ -z "${latest_tag}" ]]; then
err "Could not resolve latest n_signer release tag from API."
err "Set NSIGNER_VERSION explicitly (e.g. NSIGNER_VERSION=v0.0.11)."
err "Set NSIGNER_VERSION explicitly (e.g. NSIGNER_VERSION=v0.0.46)."
exit 1
fi

View File

@@ -0,0 +1,387 @@
# Proposal: Algorithm-Based API + New Policy Model
## 1. Problem
The current API keys off **roles** that bundle three concepts together:
- **Algorithm** (curve: secp256k1, ed25519, ml-dsa-65, etc.)
- **Purpose** (nostr, ssh, age, pq-sig, pq-kem)
- **Derivation index** (nostr_index or role_path)
The caller must know the role name and the role's configuration determines the algorithm. This is unintuitive — a caller that wants to "sign something with ed25519" shouldn't need to know that the operator named the role "ssh_main" and configured it with purpose=ssh.
The purpose field is really a **policy/enforcement constraint**, not something the caller should care about. The caller knows what operation they want (sign, verify, encapsulate) and what algorithm they want to use. The signer's job is to check whether that combination is allowed and whether the caller is approved.
## 2. Proposed API shape
### 2.1 Core principle: verb + algorithm, not verb + role
The caller specifies:
- **What** they want to do (the verb)
- **Which algorithm** they want to use
- **Which key** (by derivation index)
The signer determines:
- Whether the verb+algorithm combination is valid (enforcement)
- Whether the caller is approved (policy)
- Derives the key on demand from the mnemonic
### 2.2 Verbs
Simplify to operation-based verbs. The algorithm is a parameter, not implicit in the verb name.
| Verb | Description | Algorithm parameter | Key parameter |
|---|---|---|---|
| `get_public_key` | Get public key for a derived key | `algorithm` | `index` |
| `sign` | Sign arbitrary bytes | `algorithm` | `index` |
| `verify` | Verify a signature | `algorithm` | `index` (or `public_key`) |
| `encapsulate` | KEM encapsulation | `algorithm` | `public_key` (peer's) |
| `decapsulate` | KEM decapsulation | `algorithm` | `index` |
| `derive_shared_secret` | ECDH key agreement (x25519) | `algorithm` | `index` + `peer_public_key` |
| `sign_event` | Sign a Nostr event (secp256k1 only) | — (always secp256k1) | `index` |
| `nip44_encrypt` | NIP-44 encrypt (secp256k1 only) | — | `index` + `peer_pubkey` |
| `nip44_decrypt` | NIP-44 decrypt (secp256k1 only) | — | `index` + `peer_pubkey` |
| `nip04_encrypt` | NIP-04 encrypt (secp256k1 only) | — | `index` + `peer_pubkey` |
| `nip04_decrypt` | NIP-04 decrypt (secp256k1 only) | — | `index` + `peer_pubkey` |
| `mine_event` | Mine + sign Nostr event (secp256k1 only) | — | `index` |
The Nostr-specific verbs (`sign_event`, `nip44_*`, `nip04_*`, `mine_event`) are inherently secp256k1 — that's a protocol requirement of Nostr, not a policy choice. They don't take an `algorithm` parameter. The generic verbs (`sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`) take an `algorithm` parameter.
### 2.3 Request format
**Generic sign (any signature algorithm):**
```json
{
"id": "1",
"method": "sign",
"params": [
"68656c6c6f",
{
"algorithm": "ml-dsa-65",
"index": 0
}
]
}
```
- First param: message bytes as hex
- Second param: options with `algorithm` and `index`
**Response:**
```json
{
"id": "1",
"result": {
"signature": "<hex>",
"algorithm": "ml-dsa-65",
"key_id": "a1b2c3d4e5f6a1b2"
}
}
```
**Get public key (any algorithm):**
```json
{
"id": "2",
"method": "get_public_key",
"params": [
{
"algorithm": "ed25519",
"index": 0
}
]
}
```
**Response:**
```json
{
"id": "2",
"result": {
"algorithm": "ed25519",
"public_key": "<hex>",
"key_id": "a1b2c3d4e5f6a1b2"
}
}
```
**KEM encapsulate:**
```json
{
"id": "3",
"method": "encapsulate",
"params": [
"<peer_pubkey_hex>",
{
"algorithm": "ml-kem-768"
}
]
}
```
**KEM decapsulate:**
```json
{
"id": "4",
"method": "decapsulate",
"params": [
"<ciphertext_hex>",
{
"algorithm": "ml-kem-768",
"index": 0
}
]
}
```
**ECDH shared secret (x25519):**
```json
{
"id": "5",
"method": "derive_shared_secret",
"params": [
"<peer_pubkey_hex>",
{
"algorithm": "x25519",
"index": 0
}
]
}
```
**Nostr sign_event (unchanged, always secp256k1):**
```json
{
"id": "6",
"method": "sign_event",
"params": [
"<event_json>",
{
"index": 0
}
]
}
```
### 2.4 Algorithm names
| String | Algorithm | Key type |
|---|---|---|
| `secp256k1` | secp256k1 (Schnorr) | Signature |
| `ed25519` | ed25519 | Signature |
| `ml-dsa-65` | ML-DSA-65 (FIPS 204) | Signature |
| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205) | Signature |
| `x25519` | X25519 (ECDH) | Key agreement |
| `ml-kem-768` | ML-KEM-768 (FIPS 203) | KEM |
### 2.5 Key identification
Keys are identified by **algorithm + index** instead of role names. The index is the BIP-44 account field in the derivation path:
| Algorithm | Derivation path for index N |
|---|---|
| secp256k1 | `m/44'/1237'/N'/0/0` (NIP-06, unchanged) |
| ed25519 | `m/44'/102001'/N'/0'/0'` |
| x25519 | `m/44'/102002'/N'/0'/0'` |
| ml-dsa-65 | `m/44'/102003'/N'/0'/0'` |
| slh-dsa-128s | `m/44'/102004'/N'/0'/0'` |
| ml-kem-768 | `m/44'/102005'/N'/0'/0'` |
The `key_id` (first 16 hex chars of the public key) provides a short human-readable identifier for display at the approval prompt.
### 2.6 Backward compatibility
The existing role-based API continues to work for Nostr clients:
- `sign_event` with `{role: "main"}` or `{nostr_index: 0}` — unchanged
- `nip44_encrypt`, `nip04_decrypt`, etc. — unchanged
- `get_public_key` with `{role: "main"}` — returns plain hex (secp256k1 backward compat)
The new algorithm-based API is additive — new verbs (`sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`) use the algorithm parameter. Old verbs keep their role-based selectors.
## 3. Proposed policy model
### 3.1 Current model (role-based)
From [`plans/deny_by_default_approvals.md`](plans/deny_by_default_approvals.md):
- Roles are pre-configured at startup with purpose+curve+index
- Policy entries approve callers for specific roles
- `--preapprove caller=uid:1000,role=main,verb=sign_event`
- Prompt `[a]` approves the caller for the role
### 3.2 New model (algorithm-based)
Policy entries approve callers for **algorithm + index ranges + verbs**:
**Policy entry structure:**
```
caller: <caller_id>
algorithms: [ed25519, ml-dsa-65] # which algorithms this caller can use
index_range: 0-4 # which derivation indices (or * for any)
verbs: [sign, verify, get_public_key] # which verbs
prompt: first_per_boot # prompt behavior
```
**`--preapprove` CLI flag:**
```bash
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign
nsigner --preapprove caller=uid:1000,algorithm=ml-dsa-65,index=0,verb=sign,verify
nsigner --preapprove caller=pubkey:abc...,algorithm=ml-kem-768,index=0,verb=decapsulate
```
**Prompt display:**
When a caller requests `sign` with `algorithm=ml-dsa-65, index=0`, the prompt shows:
```
Caller: uid:1000
Action: sign with ML-DSA-65
Key: index 0 (key_id: a1b2c3d4e5f6a1b2)
[a] approve [d] deny
```
The operator approves the caller for that algorithm+index+verb combination. Subsequent requests with the same combination are auto-approved (if `prompt: first_per_boot`).
### 3.3 Nostr verbs policy
Nostr verbs (`sign_event`, `nip44_*`, `nip04_*`, `mine_event`) are always secp256k1. They can be policy-keyed as either:
- **Algorithm-based**: `algorithm=secp256k1, verb=sign_event, index=0` (new style)
- **Role-based**: `role=main, verb=sign_event` (old style, backward compat)
Both resolve to the same key. The signer accepts both selector forms.
### 3.4 Wildcards
| Pattern | Meaning |
|---|---|
| `algorithm=*` | Any algorithm |
| `index=*` | Any index |
| `index=0-4` | Indices 0 through 4 inclusive |
| `verb=*` | Any verb valid for the algorithm |
Wildcards make policy more compact but less precise. The default is no wildcards (explicit per-algorithm, per-index, per-verb).
### 3.5 Enforcement matrix
The signer checks verb+algorithm validity (regardless of policy):
| Verb | Valid algorithms | Notes |
|---|---|---|
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Generic signing |
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Generic verification |
| `encapsulate` | ml-kem-768 | KEM encapsulation |
| `decapsulate` | ml-kem-768 | KEM decapsulation |
| `derive_shared_secret` | x25519 | ECDH key agreement |
| `get_public_key` | all | Any algorithm |
| `sign_event` | secp256k1 (implicit) | Nostr protocol requirement |
| `nip44_encrypt` | secp256k1 (implicit) | Nostr protocol requirement |
| `nip44_decrypt` | secp256k1 (implicit) | Nostr protocol requirement |
| `nip04_encrypt` | secp256k1 (implicit) | Nostr protocol requirement |
| `nip04_decrypt` | secp256k1 (implicit) | Nostr protocol requirement |
| `mine_event` | secp256k1 (implicit) | Nostr protocol requirement |
If a caller requests `sign` with `algorithm=x25519`, the signer rejects with `algorithm_not_supported_for_verb` — x25519 is a key agreement algorithm, not a signature algorithm.
### 3.6 No purpose field
The `purpose` field is **removed entirely**. There is no `purpose` in the API, no purpose in enforcement, no purpose in policy. The role_purpose_t enum and all purpose-related code is removed from the codebase.
Enforcement is purely verb+algorithm based — the signer checks whether the requested verb is valid for the requested algorithm. No "purpose mismatch" errors, no purpose in policy entries, no purpose in role configuration.
Roles (if kept for backward compat with existing Nostr clients) are keyed on algorithm+index only, not purpose+curve.
## 4. Migration path
### Phase A: Add algorithm-based verbs (additive, no breaking changes)
1. Add new verbs: `sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`
2. These accept `{algorithm, index}` in the options
3. Old verbs (`sign_data`, `ssh_sign`, `kem_encapsulate`, `kem_decapsulate`) become aliases that map to the new verbs
4. Old role-based selectors continue to work on all verbs
5. Policy model extended to support algorithm-based entries alongside role-based entries
### Phase B: Deprecate old verb names (optional, future)
1. `sign_data``sign`
2. `ssh_sign``sign` (with `algorithm=ed25519`)
3. `kem_encapsulate``encapsulate`
4. `kem_decapsulate``decapsulate`
5. `verify_signature``verify`
6. Old names still work but are documented as deprecated
### Phase C: Remove purpose from enforcement (optional, future)
1. Purpose becomes pure metadata
2. Enforcement only checks verb+algorithm validity
3. Roles become optional (algorithm+index is the primary key selector)
## 5. Example: full session with new API
### Operator starts n_signer:
```bash
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0,verb=sign,verify,get_public_key \
--preapprove caller=uid:1000,algorithm=ml-dsa-65,index=0,verb=sign,verify,get_public_key \
--preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate,get_public_key
```
### Client gets an ed25519 public key:
```json
{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}
{"id":"1","result":{"algorithm":"ed25519","public_key":"...","key_id":"a1b2..."}}
```
### Client signs a message with ed25519:
```json
{"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}
{"id":"2","result":{"signature":"...","algorithm":"ed25519","key_id":"a1b2..."}}
```
### Client signs a message with ML-DSA-65:
```json
{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ml-dsa-65","index":0}]}
{"id":"3","result":{"signature":"...","algorithm":"ml-dsa-65","key_id":"c3d4..."}}
```
### Client encapsulates with ML-KEM-768 (using peer's public key):
```json
{"id":"4","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]}
{"id":"4","result":{"ciphertext":"...","shared_secret":"...","algorithm":"ml-kem-768"}}
```
### Client decapsulates (signer side, using its ML-KEM private key):
```json
{"id":"5","method":"decapsulate","params":["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]}
{"id":"5","result":{"shared_secret":"...","algorithm":"ml-kem-768"}}
```
### Client signs a Nostr event (backward compatible):
```json
{"id":"6","method":"sign_event","params":["<event_json>",{"index":0}]}
{"id":"6","result":"<signed_event_json>"}
```
## 6. Comparison: old vs new
| Aspect | Old (role-based) | New (algorithm-based) |
|---|---|---|
| Caller specifies | Role name | Algorithm + index |
| Algorithm determined by | Role's configured curve | Explicit `algorithm` parameter |
| Purpose | Enforcement constraint | Optional metadata |
| Policy unit | Role | Algorithm + index + verb |
| Nostr verbs | Role-based selector | Index-based (algorithm implicit) |
| Generic verbs | `sign_data` (role-based) | `sign` (algorithm-based) |
| Key derivation | Role's index/path | Algorithm's derivation path + index |
## 7. Decisions
1. **secp256k1 `sign` — offer both Schnorr and ECDSA.** The `sign` verb accepts an optional `scheme` parameter for secp256k1: `"scheme": "schnorr"` (default, BIP-340) or `"scheme": "ecdsa"`. Other algorithms have one signature scheme each and ignore this parameter.
2. **Index is per-algorithm.** Each algorithm has its own derivation path prefix (coin type), so "index 0" for ed25519 and "index 0" for ml-dsa-65 are different keys. This is the only sensible model — different algorithms need different key material.
3. **Key selector is `index` only.** No `role_path` support for the new verbs. The `index` parameter maps to the account field in the standard BIP-44 derivation path. This is simpler, covers all normal use cases, and is easy to policy-gate. Arbitrary derivation paths can be a future advanced feature if needed.
4. **`verify` uses the signer's own derived public key.** The `verify` verb takes `algorithm` + `index` (same as `sign`) and verifies a signature against the signer's own derived public key. This is useful for round-trip testing (client signs, then verifies). The signer does NOT verify arbitrary external signatures with a `public_key` parameter — that's a client-side operation that doesn't need the signer.
5. **Remove the `purpose` field entirely.** No `purpose` in the API, no purpose in enforcement, no purpose in policy. Enforcement is purely verb+algorithm based:
- `sign` works with any signature algorithm (secp256k1, ed25519, ml-dsa-65, slh-dsa-128s)
- `encapsulate`/`decapsulate` work with ml-kem-768
- `derive_shared_secret` works with x25519
- `sign_event`/`nip44_*`/`nip04_*`/`mine_event` always use secp256k1 (Nostr protocol requirement, not a purpose check)
- `get_public_key` works with any algorithm
The `role_purpose_t` enum and all purpose-related code is removed. Roles (if kept for backward compat) are keyed on algorithm+index only.

430
plans/mine_event_pow.md Normal file
View File

@@ -0,0 +1,430 @@
# Plan: Add `mine_event` Verb (NIP-13 Proof-of-Work) to n_signer
## Executive Summary
**Recommendation: Do NOT import event_miner as a subrepo or copy its code.**
n_signer already has [`nip013.h`](resources/nostr_core_lib/nostr_core/nip013.h:1) and [`nip013.c`](resources/nostr_core_lib/nostr_core/nip013.c:1) in its `resources/nostr_core_lib/`. event_miner is a standalone CLI tool (~716 lines) whose useful logic is ~50 lines. The rest is CLI parsing, stdin/file I/O, signal handlers, and `exit()` calls that are inappropriate for a long-running server.
Instead, we add a new `mine_event` verb to n_signer's existing dispatcher that:
1. Mines PoW for a time budget (e.g., "mine for 30 seconds with 4 threads")
2. Returns the **best result found** within that time — regardless of whether the target difficulty was reached
3. Signs the event with the role's derived private key
4. Returns the mined + signed event JSON along with metadata about the achieved difficulty
## Why Not Subrepo or Copy?
| Factor | Subrepo | Copy | New verb (recommended) |
|--------|---------|------|----------------------|
| Key management | event_miner takes raw nsec CLI arg | same | Uses n_signer's role table + key_store (secure, mlock'd) |
| Architecture | CLI tool, calls exit() | same | Fits JSON-RPC dispatcher/enforcement/policy model |
| Threading | Global vars, signal handlers, exit() | same | Clean detached thread per request, no globals |
| Best-effort model | No — stops at target or max_attempts | same | Yes — returns best result within time budget |
| Code reuse | ~50 lines useful | same | Rewrite ~150 lines for server-safe best-effort model |
| Maintenance | Extra submodule to track | Drift risk | Single codebase, single nostr_core_lib |
## Design Decisions
- **Verb name**: `mine_event`
- **Params**: `[event_json, {difficulty: N, threads: N, timeout_sec: N, ...role_selector}]`
- **Dual termination model**: Both `timeout_sec` and `difficulty` are independent options. Either, both, or at least one must be specified:
- **Both set**: Mine until target reached OR timeout — return best result (with `target_reached` flag)
- **Only `timeout_sec`**: Mine for the full duration, return best result found
- **Only `difficulty`**: Mine until target reached, with a safety max timeout (e.g., 10 minutes) to prevent infinite mining
- **Neither**: Error — must specify at least one termination condition
- **Best-effort return**: The response always includes the best event found, plus metadata (`achieved_difficulty`, `target_difficulty`, `target_reached`, `elapsed_sec`, `attempts`)
- **Threading**: Spawn a detached pthread for mining. Server stays responsive. Mining thread writes result to client socket when done.
- **Key source**: Uses the role's derived private key from `key_store` (same as `sign_event`)
## Why Our Own Mining Loop (Not `nostr_add_proof_of_work()`)
The existing [`nostr_add_proof_of_work()`](resources/nostr_core_lib/nostr_core/nip013.c:121) has two problems for the best-effort model:
1. **Discards work on failure**: It returns `NOSTR_ERROR_CRYPTO_FAILED` if `max_attempts` is exceeded without reaching the target. The best nonce found is lost.
2. **No time-based termination**: It uses `max_attempts` (a count), not a time deadline. For a "mine for 30 seconds" model, we need time-based termination.
Instead, `miner.c` implements its own mining loop that:
- Iterates nonces across multiple threads
- Uses [`nostr_create_and_sign_event()`](resources/nostr_core_lib/nostr_core/nip001.h:1) from nip001 to sign each attempt
- Uses [`nostr_calculate_pow_difficulty()`](resources/nostr_core_lib/nostr_core/nip013.h:42) from nip013 to check difficulty
- Tracks the best event (highest difficulty) across all threads
- Stops when target reached OR timeout
- Returns the best event with metadata
This reuses the library's crypto/JSON primitives without modifying the shared library.
## Request/Response Format
### Request
```json
{
"id": "req-1",
"method": "mine_event",
"params": [
"{\"kind\":1,\"content\":\"Hello Nostr!\",\"tags\":[],\"created_at\":1723666800}",
{
"difficulty": 20,
"threads": 4,
"timeout_sec": 30,
"role": "main"
}
]
}
```
- `difficulty` (optional): Target leading zero bits. If reached, mining stops early. If not specified, mining runs until timeout.
- `threads` (optional, default: 1): Number of mining threads.
- `timeout_sec` (optional): How long to mine in seconds. If not specified, mining runs until target difficulty is reached (with a safety max of 10 minutes).
- **At least one of `difficulty` or `timeout_sec` must be specified.** If neither is provided, returns error `1007` (`mining_failed` / `no_termination_condition`).
- **If only `difficulty` is specified**: A safety max timeout of 600 seconds (10 min) is applied to prevent infinite mining.
- **If only `timeout_sec` is specified**: Mining runs the full duration and returns the best result found.
- **If both are specified**: Mining stops when either condition is met (target reached OR timeout elapsed).
### Success Response (best-effort, always returned if mining ran)
```json
{
"id": "req-1",
"result": {
"event": "{\"kind\":1,\"content\":\"Hello Nostr!\",\"pubkey\":\"...\",\"id\":\"00000...\",\"sig\":\"...\",\"tags\":[[\"nonce\",\"12345\",\"20\"]],\"created_at\":1755197090}",
"achieved_difficulty": 18,
"target_difficulty": 20,
"target_reached": false,
"elapsed_sec": 30,
"attempts": 4523456
}
}
```
The `result` is now a JSON **object** (not a string like other verbs) containing the signed event plus mining metadata. The `event` field within it is the signed event JSON string. This is a departure from the other verbs that return a plain string result, but the metadata is essential for the client to know whether the target was met.
### Error Responses
- `1007` - `mining_failed` — internal error (invalid event, bad params, crypto failure)
- `1008` - `mining_busy` — another mining operation is already running (optional: reject or queue)
- Existing error codes (1001-1006, -326xx) apply as usual for policy/selector/enforcement errors
Note: Timeout is NOT an error — it's the normal termination condition. The best result is always returned.
## Architecture Diagram
```mermaid
flowchart TD
Client -->|JSON-RPC mine_event| ServerHandleOne
ServerHandleOne -->|policy_check and enforce_verb_role| PolicyEnforcement
PolicyEnforcement -->|ALLOWED| SpawnDetachedThread
ServerHandleOne -->|returns immediately| ServerLoop
SpawnDetachedThread --> MinerCoordinator
MinerCoordinator -->|spawn N worker threads| Worker1
MinerCoordinator -->|spawn N worker threads| Worker2
MinerCoordinator -->|spawn N worker threads| WorkerN
Worker1 -->|nostr_create_and_sign_event| NIP001Lib
Worker2 -->|nostr_create_and_sign_event| NIP001Lib
WorkerN -->|nostr_create_and_sign_event| NIP001Lib
NIP001Lib -->|signed event with nonce| CalcDifficulty
CalcDifficulty -->|nostr_calculate_pow_difficulty| NIP013Lib
NIP013Lib -->|difficulty bits| TrackBest
TrackBest -->|mutex protected| BestEvent
MinerCoordinator -->|timeout or target reached| JoinThreads
JoinThreads -->|best event + metadata| BuildResponse
BuildResponse -->|transport_send_framed| Client
```
## Implementation Steps
### Step 1: Add nip013 to nostr_core_lib build
The Makefile's `lib` target runs:
```
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
```
Add `13` to the `--nips` flag so nip013.c is compiled into the static library.
**Files to modify:**
- [`Makefile`](Makefile:54) — change `--nips=1,4,6,19,44` to `--nips=1,4,6,13,19,44`
### Step 2: Add VERB_MINE_EVENT to enforcement.c
Add `mine_event` to the known nostr verbs so it passes enforcement.
**Files to modify:**
- [`src/enforcement.c`](src/enforcement.c:200) — add `#define VERB_MINE_EVENT "mine_event"` and add it to [`is_nostr_verb()`](src/enforcement.c:453)
- [`src/dispatcher.c`](src/dispatcher.c:200) — add the same `#define VERB_MINE_EVENT "mine_event"` (the headerless decls pattern means defines are repeated per-file)
### Step 3: Create src/miner.c — Multithreaded Best-Effort Mining Coordinator
This is the core new file. It implements a server-safe, best-effort mining loop.
**Key design:**
- No global variables (all state in context structs)
- No `exit()` calls (return result codes)
- No signal handlers (server handles signals)
- Time-based termination (not attempt-count-based)
- Tracks best event across all threads
- Returns best event + metadata regardless of whether target was reached
**Key structures:**
```c
typedef struct {
cJSON *best_event; /* best event found so far (mutex-protected) */
int best_difficulty; /* difficulty of best_event */
uint64_t total_attempts; /* total attempts across all threads */
int target_difficulty; /* 0 = no target, mine full timeout */
int target_reached; /* 1 if target was reached */
pthread_mutex_t mutex; /* protects best_event, best_difficulty, total_attempts */
volatile int stop; /* set by coordinator when target reached or timeout */
} mine_shared_state_t;
typedef struct {
mine_shared_state_t *shared;
cJSON *event_template; /* this thread's copy of the event */
unsigned char private_key[32];
int thread_id;
uint64_t nonce_start; /* starting nonce for this thread (thread_id * stride) */
uint64_t nonce_stride; /* increment per iteration to avoid overlap */
time_t deadline; /* absolute time to stop */
uint64_t attempts; /* this thread's attempt count */
} miner_worker_ctx_t;
typedef struct {
cJSON *best_event; /* caller frees */
int achieved_difficulty;
int target_difficulty;
int target_reached;
int elapsed_sec;
uint64_t total_attempts;
} mine_result_t;
```
**Main function:**
```c
/* Returns 0 on success (result populated with best event found), -1 on error */
int miner_run(cJSON *event, const unsigned char *private_key,
int target_difficulty, int thread_count, int timeout_sec,
mine_result_t *result);
```
**Mining loop (per thread):**
```c
while (!shared->stop && time(NULL) < deadline) {
/* Build event with current nonce */
cJSON *working_tags = cJSON_Duplicate(original_tags, 1);
update_nonce_tag(working_tags, nonce, target_difficulty);
cJSON *signed_event = nostr_create_and_sign_event(kind, content, working_tags,
private_key, timestamp);
cJSON_Delete(working_tags);
/* Check difficulty */
const char *id = cJSON_GetStringValue(cJSON_GetObjectItem(signed_event, "id"));
int difficulty = nostr_calculate_pow_difficulty(id);
/* Track best under mutex */
pthread_mutex_lock(&shared->mutex);
shared->total_attempts++;
if (difficulty > shared->best_difficulty) {
if (shared->best_event) cJSON_Delete(shared->best_event);
shared->best_event = cJSON_Duplicate(signed_event, 1);
shared->best_difficulty = difficulty;
}
if (target_difficulty > 0 && difficulty >= target_difficulty) {
shared->target_reached = 1;
shared->stop = 1;
}
pthread_mutex_unlock(&shared->mutex);
cJSON_Delete(signed_event);
nonce += nonce_stride;
attempts++;
}
```
**Files to create:**
- `src/miner.c` — mining coordinator (~250 lines)
### Step 4: Add crypto_mine_event() to key_store.c
Add a function that:
1. Parses the event JSON
2. Gets the role's private key from key_store
3. Calls `miner_run()` from miner.c
4. Builds the response JSON object (event string + metadata)
5. Zeroizes the private key copy
6. Returns the response JSON string (caller frees)
**Signature:**
```c
/* Returns newly-allocated JSON string with result object, or NULL on error.
* Caller frees. */
char *crypto_mine_event(const key_store_t *store, int role_index,
const char *event_json, int difficulty,
int threads, int timeout_sec);
```
**Response building:**
```c
cJSON *result_obj = cJSON_CreateObject();
cJSON *event_item = cJSON_CreateString(signed_event_json);
cJSON_AddItemToObject(result_obj, "event", event_item);
cJSON_AddNumberToObject(result_obj, "achieved_difficulty", result.achieved_difficulty);
cJSON_AddNumberToObject(result_obj, "target_difficulty", result.target_difficulty);
cJSON_AddBoolToObject(result_obj, "target_reached", result.target_reached);
cJSON_AddNumberToObject(result_obj, "elapsed_sec", result.elapsed_sec);
cJSON_AddNumberToObject(result_obj, "attempts", (double)result.total_attempts);
char *out = cJSON_PrintUnformatted(result_obj);
cJSON_Delete(result_obj);
return out;
```
**Files to modify:**
- [`src/key_store.c`](src/key_store.c:456) — add `#include <nostr_core/nip013.h>`, add `crypto_mine_event()` function, add forward declaration in the headerless decls section
### Step 5: Update Makefile
Add `miner.c` to SOURCES. pthread is already linked (`-lpthread` in LDFLAGS).
**Files to modify:**
- [`Makefile`](Makefile:13) — add `$(SRC_DIR)/miner.c \` to SOURCES list
### Step 6: Add dispatcher handler for mine_event
In [`dispatcher_handle_request()`](src/dispatcher.c:545), add a branch for `VERB_MINE_EVENT` that:
1. Extracts `event_json` from params[0]
2. Extracts `difficulty`, `threads`, `timeout_sec` from the options object (params[last])
3. Validates: at least one of `difficulty` or `timeout_sec` must be specified (else error `1007`); `threads` defaults to 1 (max 32); if only `difficulty` is set, safety timeout of 600 sec is applied
4. Calls `crypto_mine_event()`
5. Returns the result
**Files to modify:**
- [`src/dispatcher.c`](src/dispatcher.c:675) — add `else if (strcmp(method, VERB_MINE_EVENT) == 0)` branch after the `VERB_SIGN_EVENT` branch
### Step 7: Modify server.c for async mining
The current [`server_handle_one()`](src/server.c:1453) is synchronous. For `mine_event`, we need to:
1. Detect `mine_event` method after policy check passes
2. Spawn a detached thread that:
- Calls `dispatcher_handle_request()` (which calls `crypto_mine_event()`)
- Sends the response via `transport_send_framed()` on the client_fd
- Closes the client_fd
3. Return immediately from `server_handle_one()` without closing the fd (the thread owns it now)
**Thread function:**
```c
typedef struct {
int client_fd;
dispatcher_ctx_t *dispatcher;
char *request;
} mine_thread_arg_t;
static void *mine_event_thread(void *arg) {
mine_thread_arg_t *a = (mine_thread_arg_t *)arg;
char *response = dispatcher_handle_request(a->dispatcher, a->request);
if (response != NULL) {
(void)transport_send_framed(a->client_fd, response);
free(response);
}
free(a->request);
close(a->client_fd);
free(a);
return NULL;
}
```
**In server_handle_one**, after `pchk == POLICY_ALLOW` and before calling `dispatcher_handle_request()`:
```c
if (pchk == POLICY_ALLOW && strcmp(method, "mine_event") == 0) {
pthread_t tid;
mine_thread_arg_t *arg = malloc(sizeof(*arg));
if (arg == NULL) {
/* fall through to error path */
} else {
arg->client_fd = client_fd;
arg->dispatcher = ctx->dispatcher;
arg->request = request; /* transfer ownership */
request = NULL; /* prevent free in cleanup */
if (pthread_create(&tid, NULL, mine_event_thread, arg) == 0) {
pthread_detach(tid);
/* Skip synchronous response path — thread owns fd and request */
/* Log activity and return without closing fd */
verdict = "ALLOWED";
source_label = "async-mine";
/* ... log activity ... */
return 1;
}
/* pthread_create failed — fall through to error path */
free(arg->request);
free(arg);
}
}
```
**Files to modify:**
- [`src/server.c`](src/server.c:1755) — add mine_event async path before the synchronous `dispatcher_handle_request()` call
### Step 8: Add client demos
Add `mine_event` examples to the existing demo files.
**Files to modify:**
- [`client/demo_javascript.js`](client/demo_javascript.js:1) — add mine_event example
- [`client/demo_python.py`](client/demo_python.py:1) — add mine_event example
- [`client/demo_c99.c`](client/demo_c99.c:1) — add mine_event example
### Step 9: Add tests
Create a test that:
1. Starts nsigner with a test mnemonic
2. Sends a `mine_event` request with low difficulty (e.g., 2) and short timeout (e.g., 5 sec)
3. Verifies the response contains a signed event with a nonce tag
4. Verifies the `achieved_difficulty` >= 2 and `target_reached` is true
5. Sends a `mine_event` request with high difficulty (e.g., 30) and short timeout (e.g., 3 sec)
6. Verifies the response contains a signed event, `target_reached` is false, and `achieved_difficulty` < 30
**Files to create:**
- `tests/test_mine_event.c`
**Files to modify:**
- [`Makefile`](Makefile:37) add `TEST_MINE_EVENT_TARGET` and `test-mine-event` target
### Step 10: Update documentation
**Files to modify:**
- [`README.md`](README.md:1) add `mine_event` to the verbs table with params description
- [`client/README.md`](client/README.md:1) add mine_event usage example
## Security Considerations
1. **Private key never leaves key_store**: The mining thread receives a copy of the 32-byte private key, which is zeroized after use (following the pattern in [`crypto_sign_event()`](src/key_store.c:456))
2. **Policy enforcement applies**: `mine_event` goes through the same `policy_check()` and `enforce_verb_role()` as all other verbs
3. **Thread safety**:
- Each mining thread works on its own cJSON copies
- The shared state (best_event, best_difficulty, total_attempts) is protected by a mutex
- secp256k1 context is thread-safe for signing
4. **Resource limits**:
- Max threads capped at 32 to prevent resource exhaustion
- `timeout_sec` is required (must be > 0) to prevent infinite mining
- Optional: limit to 1 concurrent mining operation (reject with `mining_busy` if already running)
5. **Client fd ownership**: The detached thread owns the client_fd and is responsible for closing it. The main server loop must not close it.
## Concurrency Considerations
- Each mining thread uses a unique nonce stride (thread_id) to avoid duplicate work
- cJSON operations are NOT thread-safe across different cJSON objects, but each thread works on its own copies
- The result mutex protects only the shared best-event tracking
- `nostr_create_and_sign_event()` creates a new secp256k1 context per call (or uses a thread-local one) — need to verify this is thread-safe. If not, each thread may need its own context.
## File Summary
| File | Action | Description |
|------|--------|-------------|
| `Makefile` | Modify | Add nip013 to --nips, add miner.c to SOURCES, add test target |
| `src/enforcement.c` | Modify | Add VERB_MINE_EVENT to is_nostr_verb() |
| `src/dispatcher.c` | Modify | Add VERB_MINE_EVENT define + handler branch |
| `src/key_store.c` | Modify | Add crypto_mine_event() function + nip013 include |
| `src/miner.c` | Create | Multithreaded best-effort mining coordinator |
| `src/server.c` | Modify | Add async mine_event path with detached thread |
| `tests/test_mine_event.c` | Create | Integration test for mine_event |
| `client/demo_javascript.js` | Modify | Add mine_event example |
| `client/demo_python.py` | Modify | Add mine_event example |
| `client/demo_c99.c` | Modify | Add mine_event example |
| `README.md` | Modify | Document mine_event verb |
| `client/README.md` | Modify | Add mine_event usage |

View File

@@ -0,0 +1,123 @@
# Plan: nostr_core_lib client updates for new n_signer features
Status: design / ready for review.
Related:
- [`../nostr_core_lib/nostr_core/nsigner_transport.h`](../../nostr_core_lib/nostr_core/nsigner_transport.h) — transport vtable
- [`../nostr_core_lib/nostr_core/nsigner_client.h`](../../nostr_core_lib/nostr_core/nsigner_client.h) — low-level client
- [`../nostr_core_lib/nostr_core/nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h) — high-level signer
- [`../nostr_core_lib/nostr_core/nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c) — `signer_remote_params_with_selector`
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — qrexec bridge design
- [`plans/interactive_transport_selection.md`](interactive_transport_selection.md) — index whitelist
---
## 1. Gaps identified
The `nostr_core_lib` client stack was built before the recent n_signer features. Three gaps need filling:
### 1.1 No `nostr_index` selector in the high-level API
`signer_remote_params_with_selector()` ([`nostr_signer.c:287`](../../nostr_core_lib/nostr_core/nostr_signer.c:287)) only emits `{"role":"..."}`. It does not support `{"nostr_index":N}`, which is n_signer's primary key-selection mechanism for Nostr identities (NIP-06 `m/44'/1237'/N'/0/0`).
The `nostr_signer_nsigner_*` constructors take a `const char* role` parameter. There's no way to say "use nostr_index 3" through the high-level API. Callers who want a specific Nostr identity by index must drop down to the low-level `nsigner_client_call` and build params manually — which is exactly what [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) do.
### 1.2 No qrexec transport
`nsigner_transport_open_*` supports unix, tcp, serial, fds — but not qrexec. On Qubes OS, the primary inter-qube path is `qrexec-client-vm <target> qubes.NsignerRpc`, which pipes framed I/O through stdin/stdout of a subprocess. There's no transport that wraps this.
### 1.3 No error code mapping for `index_not_allowed` (2002)
n_signer now returns `{"error":{"code":2002,"message":"index_not_allowed"}}` when the index whitelist denies a request. The client should surface this distinctly from `policy_denied` (2001) so callers can tell "this index isn't whitelisted" vs "this caller isn't approved".
---
## 2. Proposed changes
### 2.1 Add `nostr_index` selector to the high-level API
**Option A (minimal):** Add a new constructor variant that takes `nostr_index` instead of `role`:
```c
nostr_signer_t* nostr_signer_nsigner_unix_index(const char* socket_name, int nostr_index, int timeout_ms);
nostr_signer_t* nostr_signer_nsigner_tcp_index(const char* host, int port, int nostr_index, int timeout_ms);
/* etc. */
```
**Option B (cleaner):** Replace the `role` string parameter with a selector struct:
```c
typedef struct {
const char* role; /* NULL = use default */
int nostr_index; /* -1 = not set */
const char* role_path; /* NULL = not set */
} nsigner_selector_t;
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const nsigner_selector_t* selector, int timeout_ms);
```
**Option C (additive, chosen):** Keep existing constructors, add a setter for nostr_index:
```c
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
```
This sets the index on the remote backend, and `signer_remote_params_with_selector` emits `{"nostr_index":N}` instead of `{"role":"..."}` when set. Existing callers using `role` are unaffected.
**Decision: Option C** — additive, no API break, smallest change.
### 2.2 Add qrexec transport
New transport constructor:
```c
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
```
Implementation: `fork()` + `execvp("qrexec-client-vm", [target_qube, service_name])` with `stdin`/`stdout` pipes. The `send_framed`/`recv_framed` vtable methods write/read through the pipes. `reconnect` re-spawns the subprocess (qrexec handles one request per invocation, so reconnect is mandatory before each call — same as the existing per-request reconnect pattern).
The transport is stateless per call: each `reconnect` spawns a fresh `qrexec-client-vm` process, sends one framed request, reads one framed response, and the process exits. This matches the qrexec execution model.
### 2.3 Add `nostr_signer_nsigner_qrexec` high-level constructor
```c
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
```
Wraps `nsigner_transport_open_qrexec` + `nostr_signer_nsigner_from_transport`. No auth envelope needed — qrexec identity comes from `QREXEC_REMOTE_DOMAIN` on the server side.
### 2.4 Surface `index_not_allowed` error distinctly
Add to `nostr_common.h`:
```c
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
```
In `nsigner_client_call` (or `nostr_signer` remote backend), when the response error code is 2002, map it to `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` so callers can distinguish it from generic policy denial.
---
## 3. Implementation checklist
In `nostr_core_lib`:
- [ ] `nsigner_transport.h` / `nsigner_transport.c`: add `nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms)` — fork+exec `qrexec-client-vm`, pipe-based framed I/O, reconnect re-spawns.
- [ ] `nostr_signer.h` / `nostr_signer.c`: add `nostr_signer_nsigner_qrexec(target_qube, service_name, role, timeout_ms)`.
- [ ] `nostr_signer.c`: add `nostr_signer_nsigner_set_nostr_index(signer, nostr_index)`; update `signer_remote_params_with_selector` to emit `{"nostr_index":N}` when index is set.
- [ ] `nostr_common.h`: add `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` error code.
- [ ] `nsigner_client.c`: map error code 2002 to the new constant.
- [ ] `NSIGNER_INTEGRATION.md`: document the qrexec transport, nostr_index selector, and index_not_allowed error.
In `n_signer` (this repo):
- [ ] Update [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) to optionally use the high-level `nostr_signer` API with `nostr_index` selector (demonstrates the new client API).
- [ ] Add a C example for the qrexec path using the new `nostr_signer_nsigner_qrexec` constructor.
---
## 4. Open questions
1. **Should the qrexec transport live in `nostr_core_lib` or in a Qubes-specific addon?** The transport is only useful on Qubes OS, but `nostr_core_lib` is meant to be cross-platform. **Decision: put it in `nostr_core_lib` behind `NOSTR_ENABLE_NSIGNER_CLIENT` — it just won't work on non-Qubes hosts (execvp fails). No new feature flag needed.**
2. **Should `nostr_signer_nsigner_set_nostr_index` override `role` or coexist?** n_signer rejects ambiguous selectors (both `role` and `nostr_index` set = `ambiguous_role_selector`). **Decision: setting nostr_index clears role, and vice versa. The setter is exclusive.**

View File

@@ -0,0 +1,567 @@
# Plan: Post-Quantum Cryptography + Standard ECC Expansion for n_signer
## 1. Goal
Expand n_signer's crypto palette from the current single algorithm (secp256k1 for Nostr) to a complete offering:
**Standard ECC (classical):**
- `ed25519` — SSH signing keys (works with current OpenSSH), general-purpose signatures
- `x25519` — key agreement (age encryption, WireGuard-style identities, SSH KEX classical leg)
**Post-quantum (NIST FIPS standardized):**
- `ML-DSA-65` (FIPS 204, lattice-based signatures) — post-quantum digital signatures
- `SLH-DSA-128s` (FIPS 205, hash-based signatures) — post-quantum signatures with minimal trust assumptions
- `ML-KEM-768` (FIPS 203, lattice-based KEM) — post-quantum key encapsulation for encryption
All six algorithms are always compiled in on all targets (host x86_64 static binary and ESP32 firmware). Size is acceptable on both; signing speed for SLH-DSA-128s on ESP32 may be slow (5-30s) but the user accepts this and will choose whether to use it per-role.
## 2. Background and context
### 2.1 Current state
n_signer models three curves in [`role_curve_t`](src/role_table.c:89) (`secp256k1`, `ed25519`, `x25519`) and five purposes in [`role_purpose_t`](src/role_table.c:79) (`nostr`, `bitcoin`, `ssh`, `age`, `fips`). However, only `PURPOSE_NOSTR + CURVE_SECP256K1` is actually implemented:
- [`crypto_derive_all`](src/key_store.c:484) and [`crypto_derive_one`](src/key_store.c:548) skip any role that isn't `PURPOSE_NOSTR + CURVE_SECP256K1 + SELECTOR_NOSTR_INDEX`.
- [`enforce_verb_role`](src/enforcement.c:468) only allows nostr verbs on `PURPOSE_NOSTR + CURVE_SECP256K1`; everything else returns `ENFORCE_ERR_UNKNOWN_VERB` or a mismatch.
- [`derived_key_t`](src/key_store.c:303) uses fixed 32-byte arrays for private/public keys — insufficient for PQ keys (ML-DSA-65 priv is 4032 bytes, pub 1952 bytes).
### 2.2 PQ key derivation from mnemonic
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.
However, all three PQ algorithms internally use a seed to generate the full key pair via keygen. The approach:
1. Derive a 32-byte seed from the mnemonic using BIP-32/HMAC (same as secp256k1 today, using a PQ-specific derivation path)
2. Feed that seed into a deterministic PRNG (SHA-256-DRBG or SHAKE-256)
3. Replace PQClean's `randombytes()` callback with this PRNG so keygen is deterministic
4. The algorithm expands the seed into the full key pair
This gives **deterministic, mnemonic-recoverable PQ keys** — same mnemonic, same role, same key pair every time. The crash-equals-wipe model from [`README.md`](README.md) is preserved because we re-derive from the mnemonic on every startup. No persistence needed.
This is the same technique used by PQM4 (the MCU post-quantum benchmark project) for deterministic test vectors.
### 2.3 SSH post-quantum landscape
Per OpenSSH's PQ page (openssh.com/pq.html):
- **KEX (key agreement)**: PQ supported since OpenSSH 9.0, default since 10.0 (`mlkem768x25519-sha256`). This is session encryption, handled by the SSH protocol — n_signer is not involved.
- **Signature/authentication keys**: "OpenSSH will add support for post-quantum signature algorithms in the future." Not yet implemented. SSH signing keys are still classical (ed25519, RSA, ECDSA).
Implication: ed25519 is still required for SSH signing keys today. ML-DSA-65 is forward-looking — we build the primitive before the ecosystem supports it. When OpenSSH adds PQ signing keys, n_signer will already have ML-DSA-65 ready.
### 2.4 Library choice: PQClean
**PQClean** (public domain / CC0) is the reference implementation source for all three algorithms. Reasons:
- **ESP32-compatible**: Self-contained C, no build system dependency, proven on Cortex-M4 via PQM4. No CMake, no provider loading, no OpenSSL 3.x requirement.
- **Minimal footprint**: Vendor only the three algorithm folders. No infrastructure code from liboqs.
- **Public domain**: No license complexity for a security-critical project.
- **`randombytes()` hook**: Each algorithm calls a `randombytes()` function that we can replace with our mnemonic-seeded PRNG for deterministic keygen.
liboqs was considered but rejected: designed for servers, CMake-heavy, pulls in infrastructure code not needed on MCU, no MCU deployments. oqs-provider was rejected: requires OpenSSL 3.x provider loading, not viable on ESP32.
For ed25519 and x25519: use existing OpenSSL (already linked) or libsecp256k1's sibling curve25519 code. OpenSSL's EVP_PKEY API already supports both. On ESP32, use the ESP-IDF mbedtls component which has ed25519/x25519 support.
## 3. Design
### 3.1 Algorithm registry
Introduce a new `src/pq_crypto.c` / `pq_crypto.h` (headerless-decls style matching the project convention) that provides a unified interface over all six algorithms. The core abstraction:
```c
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
```
### 3.2 Variable-length key storage
Replace the fixed 32-byte [`derived_key_t`](src/key_store.c:303) with a variable-length design:
```c
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char key_id[128]; /* human-readable key identifier (bech32/hex/algorithm-specific) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
```
The `secure_buf_t` already supports variable sizes via [`secure_buf_alloc`](src/secure_mem.c). The change is allocating different sizes per role based on the algorithm.
### 3.3 Derivation paths
Each algorithm gets a distinct BIP-32 derivation path from the mnemonic. All paths follow the standard 5-level BIP-44 structure for consistency: `m/purpose'/coin_type'/account'/change/index`. This ensures no two algorithms derive from the same path (avoiding key reuse across algorithms) and maintains compatibility with standard HD wallet tooling.
| Algorithm | Purpose | Derivation path | Notes |
|---|---|---|---|
| secp256k1 | nostr | `m/44'/1237'/<n>'/0/0` (existing) | NIP-06, unchanged |
| secp256k1 | bitcoin | `m/44'/0'/<account>'/0/0` etc. | BIP-44, future |
| ed25519 | ssh | `m/44'/102001'/<n>'/0'/0'` | SLIP-0010 ed25519 (all hardened) |
| x25519 | age | `m/44'/102002'/<n>'/0'/0'` | SLIP-0010 x25519 (all hardened) |
| ML-DSA-65 | pq-sig | `m/44'/102003'/<n>'/0'/0'` → seed → PQClean keygen | PQ-SIG coin type |
| SLH-DSA-128s | pq-sig | `m/44'/102004'/<n>'/0'/0'` → seed → PQClean keygen | PQ-HASH-SIG coin type |
| ML-KEM-768 | pq-kem | `m/44'/102005'/<n>'/0'/0'` → seed → PQClean keygen | PQ-KEM coin type |
**Path structure notes:**
- **Purpose (level 0):** Always `44'` (BIP-44). This is the registered BIP-44 purpose for HD wallets. Using a different purpose number (e.g., 204 for PQ-SIG) at level 0 would be non-standard; instead we use distinct **coin_type** values at level 1 to separate algorithm families.
- **Coin type (level 1):** Distinct coin type per algorithm family: `1237` (Nostr, existing NIP-06), `102001` (SSH/ed25519), `102002` (age/x25519), `102003` (PQ-SIG/ML-DSA), `102004` (PQ-HASH-SIG/SLH-DSA), `102005` (PQ-KEM/ML-KEM). The 102XXX range was chosen because it is unregistered in [SLIP-44](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) and unlikely to be claimed by real cryptocurrencies, avoiding future path collisions.
- **Account (level 2):** The `<n>` index — this is the `nostr_index` or equivalent role index. Hardened (`'`).
- **Change (level 3):** `0` for secp256k1 (standard BIP-44 external chain). `0'` (hardened) for ed25519/x25519/PQ because SLIP-0010 requires all-hardened derivation.
- **Index (level 4):** `0` for secp256k1 (standard BIP-44 first address). `0'` (hardened) for ed25519/x25519/PQ, same SLIP-0010 reason.
**Why all-hardened for ed25519/x25519/PQ:** SLIP-0010 (the standard for ed25519/x25519 HD derivation) requires that every derivation step be hardened — non-hardened derivation is not possible with ed25519 because the public key cannot be used to derive child public keys (the math doesn't work the same as secp256k1). For PQ, we follow the same convention since we're using the path output as a seed, and hardened derivation provides stronger isolation between derived keys.
The path produces a 32-byte seed (512-bit extended key, of which 256 bits are the private key material). For ECC algorithms, this seed IS the private key (or is processed via SLIP-0010's derivation). For PQ algorithms, this seed feeds the deterministic PRNG (SHA-256-DRBG) that replaces PQClean's `randombytes()` during keygen.
### 3.4 New purpose values
Extend [`role_purpose_t`](src/role_table.c:79):
```c
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* new — post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* new — post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
```
### 3.5 New curve values
Extend [`role_curve_t`](src/role_table.c:89):
```c
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65, /* new */
CURVE_SLH_DSA_128S, /* new */
CURVE_ML_KEM_768, /* new */
CURVE_UNKNOWN
} role_curve_t;
```
String mappings in [`role_curve_from_str`](src/role_table.c:597) / [`role_curve_to_str`](src/role_table.c:631):
- `"ml-dsa-65"``CURVE_ML_DSA_65`
- `"slh-dsa-128s"``CURVE_SLH_DSA_128S`
- `"ml-kem-768"``CURVE_ML_KEM_768`
### 3.6 New verbs
Add verbs for the new crypto operations:
| Verb | Purpose | Curve | Description |
|---|---|---|---|
| `get_public_key` | all | all | Already exists; extend to return algorithm-appropriate pubkey format |
| `sign_data` | pq-sig, ssh | ml-dsa-65, slh-dsa-128s, ed25519 | Sign arbitrary bytes (not a Nostr event) |
| `verify_signature` | pq-sig, ssh | ml-dsa-65, slh-dsa-128s, ed25519 | Verify a signature (optional — signer can verify, or client verifies) |
| `kem_encapsulate` | pq-kem | ml-kem-768 | Generate ciphertext + shared secret from peer's ML-KEM public key |
| `kem_decapsulate` | pq-kem | ml-kem-768 | Decapsulate ciphertext to recover shared secret |
| `ssh_sign` | ssh | ed25519 | Sign SSH authentication challenge (ed25519-specific format) |
The existing nostr verbs (`sign_event`, `nip44_encrypt`, etc.) remain unchanged and still require `PURPOSE_NOSTR + CURVE_SECP256K1`.
### 3.7 Enforcement rules
Extend [`enforce_verb_role`](src/enforcement.c:468) with new verb→purpose→curve rules:
```c
/* Nostr verbs: unchanged — PURPOSE_NOSTR + CURVE_SECP256K1 only */
/* sign_data: allowed on PQ-SIG or SSH purposes */
/* PURPOSE_PQ_SIG + CURVE_ML_DSA_65 → OK */
/* PURPOSE_PQ_SIG + CURVE_SLH_DSA_128S → OK */
/* PURPOSE_SSH + CURVE_ED25519 → OK */
/* kem_encapsulate / kem_decapsulate: */
/* PURPOSE_PQ_KEM + CURVE_ML_KEM_768 → OK */
/* ssh_sign: */
/* PURPOSE_SSH + CURVE_ED25519 → OK */
```
Fail-closed for any unlisted combination, preserving the existing security model from [`plans/deny_by_default_approvals.md`](plans/deny_by_default_approvals.md).
### 3.8 Public key output format
The `get_public_key` verb currently returns a 64-hex-char secp256k1 pubkey. For PQ keys, the public keys are much larger. The response format changes to include algorithm metadata:
```json
{
"id": "...",
"result": {
"algorithm": "ml-dsa-65",
"public_key": "<hex-encoded, up to ~4KB>",
"key_id": "<short identifier for display>"
}
}
```
For backward compatibility, secp256k1/nostr roles continue to return the plain hex string (the existing format) unless the client requests the structured format via an options flag.
### 3.9 PQClean integration
Vendor PQClean into `resources/pqclean/` with only the three algorithm folders:
```
resources/pqclean/
├── crypto_sign/
│ ├── ml-dsa-65/
│ │ ├── api.h
│ │ ├── sign.c
│ │ ├── params.h
│ │ └── ... (algorithm-specific files)
│ └── slh-dsa-128s/
│ ├── api.h
│ ├── sign.c
│ ├── params.h
│ └── ...
├── crypto_kem/
│ └── ml-kem-768/
│ ├── api.h
│ ├── kem.c
│ ├── params.h
│ └── ...
├── common/ /* shared utilities (fips202, sha2, randombytes hook) */
│ ├── fips202.c
│ ├── sha2.c
│ └── randombytes.c /* we replace this with our PRNG */
└── pqclean.h /* umbrella include */
```
Each PQClean algorithm exposes a simple API:
```c
int crypto_sign_keypair(unsigned char *pk, unsigned char *sk); /* uses randombytes() */
int crypto_sign(unsigned char *sig, size_t *siglen,
const unsigned char *m, size_t mlen,
const unsigned char *sk);
int crypto_sign_open(unsigned char *m, size_t *mlen,
const unsigned char *sm, size_t smlen,
const unsigned char *pk);
int crypto_kem_keypair(unsigned char *pk, unsigned char *sk);
int crypto_kem_enc(unsigned char *ct, unsigned char *ss, const unsigned char *pk);
int crypto_kem_dec(unsigned char *ss, const unsigned char *ct, const unsigned char *sk);
```
### 3.10 Deterministic PRNG for PQ keygen
New file `src/pq_drbg.c`:
```c
/* SHA-256-DRBG seeded from mnemonic-derived seed.
* Replaces PQClean's randombytes() for deterministic keygen. */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
```
PQClean's `randombytes.c` is replaced with a shim that calls `pq_drbg_randombytes()`. During keygen, the DRBG is seeded from the mnemonic-derived path seed. After keygen, the DRBG is zeroized.
For signing operations, PQClean's sign functions do not call `randombytes()` (ML-DSA and SLH-DSA are deterministic signatures), so the DRBG is only needed during keygen.
### 3.11 ed25519 / x25519 implementation
**Host (x86_64 static binary):** Use OpenSSL's EVP_PKEY API (already linked via `-lssl -lcrypto` in [`Makefile`](Makefile:3)). Functions:
- `EVP_PKEY_keygen_init` / `EVP_PKEY_keygen` for key generation
- `EVP_DigestSign` / `EVP_DigestVerify` for ed25519 signing
- `EVP_PKEY_derive` for x25519 key agreement
**ESP32 firmware:** Use ESP-IDF's mbedtls component (`mbedtls_ed25519_*`, `mbedtls_ecdh_*` with X25519 curve). Already available in the ESP-IDF dependency tree.
The abstraction in `src/pq_crypto.c` wraps both backends behind the same interface so the dispatcher and key_store are backend-agnostic.
### 3.12 Build system changes
#### Makefile (host)
Add PQClean source files to [`SOURCES`](Makefile:13):
```makefile
PQCLEAN_DIR := resources/pqclean
PQCLEAN_SOURCES := \
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/sign.c \
$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65/... \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/sign.c \
$(PQCLEAN_DIR)/crypto_sign/slh-dsa-128s/... \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/kem.c \
$(PQCLEAN_DIR)/crypto_kem/ml-kem-768/... \
$(PQCLEAN_DIR)/common/fips202.c \
$(PQCLEAN_DIR)/common/sha2.c \
src/pq_crypto.c \
src/pq_drbg.c
SOURCES += $(PQCLEAN_SOURCES)
CFLAGS += -I$(PQCLEAN_DIR) -I$(PQCLEAN_DIR)/crypto_sign/ml-dsa-65 ...
```
#### Dockerfile.alpine-musl
No new system packages needed — PQClean is vendored C source compiled by the existing gcc/musl toolchain. The build already has `-lssl -lcrypto` for ed25519/x25519.
#### ESP32 firmware
Add PQClean sources to the firmware CMakeLists.txt. ESP32-S3 has hardware SHA-2 acceleration which benefits SLH-DSA-128s. The PQClean SHA-2 implementation can be replaced with ESP-IDF's hardware-accelerated `mbedtls_sha256` for a significant speedup.
### 3.13 Architecture diagram
```mermaid
graph TD
M[BIP-39 Mnemonic] --> DERIVE[crypto_derive_all]
DERIVE --> |m/44'/1237'/n'| SECP[secp256k1 keypair]
DERIVE --> |m/44'/1238'/n'| ED25519[ed25519 keypair]
DERIVE --> |m/44'/1239'/n'| X25519[x25519 keypair]
DERIVE --> |m/44'/204'/n' seed| DRBG[SHA-256-DRBG]
DRBG --> |randombytes hook| MLDSA[ML-DSA-65 keypair]
DERIVE --> |m/44'/205'/n' seed| DRBG2[SHA-256-DRBG]
DRBG2 --> |randombytes hook| SLHDSA[SLH-DSA-128s keypair]
DERIVE --> |m/44'/206'/n' seed| DRBG3[SHA-256-DRBG]
DRBG3 --> |randombytes hook| MLKEM[ML-KEM-768 keypair]
SECP --> KS[Key Store - variable length]
ED25519 --> KS
X25519 --> KS
MLDSA --> KS
SLHDSA --> KS
MLKEM --> KS
KS --> DISP[Dispatcher]
DISP --> |sign_event| SECP
DISP --> |sign_data| ED25519
DISP --> |sign_data| MLDSA
DISP --> |sign_data| SLHDSA
DISP --> |kem_encaps/decaps| MLKEM
DISP --> |ssh_sign| ED25519
```
## 4. Implementation phases
### Phase 1 — Foundation: variable-length key store + algorithm registry
**Goal:** Refactor the key store to support variable-length keys and introduce the algorithm abstraction. No new crypto yet — secp256k1 continues to work through the new abstraction.
**Files:**
- `src/pq_crypto.c` / `pq_crypto.h` (new) — algorithm registry, size tables, `crypto_alg_from_role()`
- `src/key_store.c` (modify) — replace fixed 32-byte `derived_key_t` with variable-length `secure_buf_t` for both priv and pub; route secp256k1 through the new abstraction
- `src/role_table.c` (modify) — add new purpose/curve enum values and string mappings
- `tests/test_key_store.c` (new or extend) — verify secp256k1 still works after refactor
**Exit criteria:**
- `make test` passes (all existing tests)
- secp256k1 key derivation and signing works through the new abstraction
- `derived_key_t` uses `secure_buf_t` for both private and public keys
- New enum values exist but are not yet wired to crypto
### Phase 2 — ed25519 + x25519 (standard ECC)
**Goal:** Implement ed25519 signing and x25519 key agreement using OpenSSL on host.
**Files:**
- `src/pq_crypto.c` (extend) — add ed25519 keygen, sign, verify; x25519 keygen, derive
- `src/key_store.c` (extend) — derive ed25519/x25519 keys from mnemonic via SLIP-0010 paths
- `src/enforcement.c` (extend) — add `sign_data` and `ssh_sign` verbs for `PURPOSE_SSH + CURVE_ED25519`
- `src/dispatcher.c` (extend) — handle new verbs
- `tests/test_pq_crypto.c` (new) — ed25519 sign/verify roundtrip, x25519 ECDH roundtrip
**Exit criteria:**
- ed25519 key derived from mnemonic, signs data, signature verifies
- x25519 key derived from mnemonic, ECDH shared secret matches between two derived keys
- `sign_data` verb works for ed25519
- Enforcement rejects ed25519 on nostr verbs and vice versa
### Phase 3 — PQClean vendoring + ML-DSA-65
**Goal:** Vendor PQClean, implement deterministic keygen via DRBG, get ML-DSA-65 working.
**Files:**
- `resources/pqclean/` (new) — vendored PQClean ML-DSA-65 + common files
- `src/pq_drbg.c` / `pq_drbg.h` (new) — SHA-256-DRBG seeded from mnemonic
- `resources/pqclean/common/randombytes.c` (replace) — shim calling `pq_drbg_randombytes()`
- `src/pq_crypto.c` (extend) — ML-DSA-65 keygen, sign, verify
- `src/key_store.c` (extend) — derive ML-DSA-65 keys via DRBG-seeded keygen
- `src/enforcement.c` (extend) — `sign_data` for `PURPOSE_PQ_SIG + CURVE_ML_DSA_65`
- `src/dispatcher.c` (extend) — handle ML-DSA-65 signing
- `Makefile` (modify) — add PQClean sources
- `Dockerfile.alpine-musl` (verify) — static build still works with PQClean
- `tests/test_pq_crypto.c` (extend) — ML-DSA-65 keygen determinism, sign/verify roundtrip, known-answer test
**Exit criteria:**
- ML-DSA-65 keypair derived deterministically from mnemonic (same mnemonic + role = same keypair)
- Signature verifies with PQClean's verify function
- Static binary builds with PQClean linked
- Known-answer test passes (test vector from NIST or PQClean)
### Phase 4 — SLH-DSA-128s
**Goal:** Add hash-based PQ signatures.
**Files:**
- `resources/pqclean/crypto_sign/slh-dsa-128s/` (new) — vendored PQClean SLH-DSA-128s
- `src/pq_crypto.c` (extend) — SLH-DSA-128s keygen, sign, verify
- `src/key_store.c` (extend) — derive SLH-DSA-128s keys
- `src/enforcement.c` (extend) — `sign_data` for `PURPOSE_PQ_SIG + CURVE_SLH_DSA_128S`
- `src/dispatcher.c` (extend) — handle SLH-DSA-128s signing
- `Makefile` (modify) — add SLH-DSA-128s sources
- `tests/test_pq_crypto.c` (extend) — SLH-DSA-128s keygen determinism, sign/verify roundtrip
**Exit criteria:**
- SLH-DSA-128s keypair derived deterministically from mnemonic
- Signature verifies
- Known-answer test passes
- Document the signing latency on ESP32 (measure and record, no mitigation required)
### Phase 5 — ML-KEM-768
**Goal:** Add PQ key encapsulation.
**Files:**
- `resources/pqclean/crypto_kem/ml-kem-768/` (new) — vendored PQClean ML-KEM-768
- `src/pq_crypto.c` (extend) — ML-KEM-768 keygen, encaps, decaps
- `src/key_store.c` (extend) — derive ML-KEM-768 keys
- `src/enforcement.c` (extend) — `kem_encapsulate` / `kem_decapsulate` for `PURPOSE_PQ_KEM + CURVE_ML_KEM_768`
- `src/dispatcher.c` (extend) — handle KEM verbs
- `Makefile` (modify) — add ML-KEM-768 sources
- `tests/test_pq_crypto.c` (extend) — ML-KEM-768 keygen determinism, encaps/decaps roundtrip, shared secret matches
**Exit criteria:**
- ML-KEM-768 keypair derived deterministically from mnemonic
- Encapsulate with public key produces ciphertext + shared secret
- Decapsulate with private key + ciphertext recovers same shared secret
- Known-answer test passes
### Phase 6 — Public key output format + client API
**Goal:** Update the `get_public_key` response to handle large PQ public keys and algorithm metadata.
**Files:**
- `src/dispatcher.c` (modify) — structured `get_public_key` response for non-secp256k1 algorithms
- `client/` (modify) — client library handles new response format
- `documents/CLIENT_IMPLEMENTATION.md` (modify) — document new verbs and response formats
- `examples/` (new) — example clients for PQ signing and KEM
**Exit criteria:**
- `get_public_key` returns algorithm-appropriate format for all 6 algorithms
- secp256k1 backward compatibility preserved (plain hex string by default)
- Client examples demonstrate PQ signing and KEM usage
### Phase 7 — ESP32 firmware port
**Goal:** Get PQ algorithms working on ESP32-S3 firmware.
**Files:**
- `firmware/feather_s3_tft/main/` (modify) — add PQClean sources to firmware build, replace SHA-2 with hardware-accelerated mbedtls
- `firmware/feather_s3_tft/CMakeLists.txt` (modify) — add PQClean source files
- `firmware/cyd_esp32_2432s028/CMakeLists.txt` (modify) — same
**Exit criteria:**
- ESP32-S3 firmware builds with all 6 algorithms
- ML-DSA-65 signing works on device (measure latency)
- SLH-DSA-128s signing works on device (measure and document latency)
- ML-KEM-768 encaps/decaps works on device
- ed25519/x25519 work on device via mbedtls
- Flash usage measured and documented (confirm it fits)
### Phase 8 — Documentation
**Files:**
- `README.md` (modify) — add PQ algorithms to the crypto palette section, document new verbs
- `documents/SECURITY.md` (modify) — PQ threat model, deterministic derivation explanation, SLH-DSA latency note
- `plans/seed_phrase_uses.md` (modify) — add PQ purpose domains to the catalog
- `documents/CLIENT_IMPLEMENTATION.md` (modify) — full verb reference for new operations
## 5. Threat model considerations
### 5.1 Deterministic PQ key derivation
The mnemonic-seeded DRBG approach is non-standard. There is no NIST or IETF specification for deriving PQ keys from a BIP-39 mnemonic. The security argument:
- The 32-byte seed from BIP-32 derivation has full 256 bits of entropy (assuming the mnemonic has full entropy).
- SHA-256-DRBG is a NIST-approved DRBG (SP 800-90A). Seeding it with 256 bits of entropy is sufficient for all three PQ algorithms.
- The alternative (random PQ keys, no mnemonic recovery) breaks n_signer's core model. The deterministic approach is the right tradeoff for this project.
**Risk:** If a weakness is found in using DRBG output as PQ keygen randomness, all PQ keys derived this way could be affected. Mitigation: the DRBG is seeded per-role with distinct derivation paths, so compromising one role's PQ key does not compromise others.
### 5.2 PQ algorithm maturity
ML-DSA, SLH-DSA, and ML-KEM are FIPS-standardized (FIPS 203, 204, 205). They have undergone extensive NIST scrutiny. However, they are newer than classical algorithms. The plan does not replace secp256k1 for Nostr — PQ algorithms are additional options, not replacements.
### 5.3 SLH-DSA-128s signing latency on ESP32
SLH-DSA-128s signing on ESP32 could take 5-30 seconds. This is not a security issue but a UX consideration. The approval prompt flow on the feather/CYD should show a "signing..." indicator during the operation. The user has accepted this and will choose whether to use SLH-DSA-128s per-role.
### 5.4 Key size and memory
PQ private keys are large (ML-DSA-65: 4032 bytes, ML-KEM-768: 2400 bytes). With `ROLE_TABLE_MAX_ENTRIES` at 256, a full table of PQ keys would use ~1MB of mlock'd memory. This is acceptable on host. On ESP32 with 512KB SRAM, the role table should be smaller or PQ roles should be derived on-demand rather than all at startup. The existing on-demand derivation in [`crypto_derive_one`](src/key_store.c:548) already supports this pattern.
## 6. What is explicitly out of scope
1. **Hybrid signatures** (e.g., ed25519 + ML-DSA combined signature). No standard exists yet for SSH. Will be a follow-up when OpenSSH defines the format.
2. **PQ SSH signing key format**. OpenSSH does not support PQ signing keys yet. We build the primitive; the SSH wire format is future work.
3. **Bitcoin purpose implementation**. `PURPOSE_BITCOIN` remains modeled but not implemented. Separate plan.
4. **FIPS purpose with PQ algorithms**. The FIPS mesh transport purpose is orthogonal to the crypto algorithm. Separate concern.
5. **PQ key persistence**. Breaks the crash-equals-wipe model. Explicitly rejected.
6. **NIP-44/NIP-04 with PQ keys**. Nostr encryption uses secp256k1 ECDH. PQ KEM is a separate encryption path, not a Nostr NIP.
7. **liboqs or oqs-provider**. Rejected in favor of PQClean for ESP32 compatibility.
## 7. Open questions
1. **PQ public key encoding for `get_public_key`**: Should PQ public keys be returned as raw hex, base64, or a structured format? Plan defaults to hex for consistency with secp256k1, but PQ pubkeys are large (ML-DSA-65 pub is 1952 bytes = 3904 hex chars). Base64 would be more compact. Decision can be made at implementation time.
2. **Derivation path purpose codes**: The plan proposes `44'/204'`, `44'/205'`, `44'/206'` for PQ-SIG, PQ-HASH-SIG, PQ-KEM. These are unregistered BIP-44 purpose codes. If there is a future standard for PQ derivation paths, these may need to change. Acceptable for now since the derivation is internal to n_signer.
3. **ESP32 role table size for PQ**: Should the firmware limit the number of PQ roles to control memory usage, or rely on on-demand derivation? Plan defaults to on-demand derivation (already supported) with no hard limit change.
4. **Verify-on-signer for `sign_data`**: Should the signer verify its own PQ signatures before returning them to the client? Adds latency but catches implementation bugs. Plan defaults to no (client verifies), matching the existing `sign_event` behavior.
## 8. Files touched summary
| File | Action | Phase |
|---|---|---|
| `src/pq_crypto.c` / `pq_crypto.h` | New | 1-5 |
| `src/pq_drbg.c` / `pq_drbg.h` | New | 3 |
| `src/key_store.c` | Modify (variable-length keys, new derivations) | 1-5 |
| `src/role_table.c` | Modify (new enum values, string mappings) | 1 |
| `src/enforcement.c` | Modify (new verb→purpose→curve rules) | 2-5 |
| `src/dispatcher.c` | Modify (new verb handlers, structured pubkey response) | 2-6 |
| `src/main.c` | Modify (headerless decls for new types) | 1-5 |
| `resources/pqclean/` | New (vendored PQClean) | 3-5 |
| `Makefile` | Modify (PQClean sources, includes) | 3-5 |
| `Dockerfile.alpine-musl` | Verify (static build with PQClean) | 3 |
| `tests/test_pq_crypto.c` | New | 2-5 |
| `firmware/feather_s3_tft/` | Modify (PQClean in firmware build) | 7 |
| `firmware/cyd_esp32_2432s028/` | Modify (same) | 7 |
| `README.md` | Modify (crypto palette, new verbs) | 8 |
| `documents/SECURITY.md` | Modify (PQ threat model) | 8 |
| `documents/CLIENT_IMPLEMENTATION.md` | Modify (new verb docs) | 6, 8 |
| `plans/seed_phrase_uses.md` | Modify (PQ purpose domains) | 8 |
| `examples/` | New (PQ signing + KEM examples) | 6 |

View File

@@ -92,15 +92,57 @@ This prevents "same seed means same trust domain" mistakes by separating identit
- Per-service deterministic auth keys (HMAC or asymmetric challenge keys depending on service model).
- Strongly policy-scoped to avoid accidental cross-service linkage.
### 3.7 Post-quantum signature identities (`purpose="pq-sig"`)
- Candidate curves: `ml-dsa-65`, `slh-dsa-128s`
- Derivation paths:
- ML-DSA-65: `m/44'/102003'/<n>'/0'/0'` → seed → SHAKE-256 DRBG → PQClean keygen
- SLH-DSA-128s: `m/44'/102004'/<n>'/0'/0'` → seed → SHAKE-256 DRBG → PQClean keygen
- Uses:
- PQ-aware protocol signatures
- future SSH PQ signing keys (when OpenSSH adds support)
- general-purpose PQ signatures for forward-looking applications
- Note: OpenSSH does not yet support PQ signing keys. These are forward-looking — the primitives are ready for when the ecosystem adopts them. See [`plans/post_quantum_crypto.md`](post_quantum_crypto.md) §2.3 for the SSH PQ landscape.
- Note: SLH-DSA-128s signing on ESP32 can take 530 seconds. Choose per-role whether the latency tradeoff is acceptable.
- Verbs: `sign_data`, `verify_signature` (see [`README.md`](../README.md) §5).
### 3.8 Post-quantum key encapsulation (`purpose="pq-kem"`)
- Candidate curves: `ml-kem-768`
- Derivation path: `m/44'/102005'/<n>'/0'/0'` → seed → SHAKE-256 DRBG → PQClean keygen
- Uses:
- PQ key agreement for encryption (encapsulate a shared secret against a peer's ML-KEM-768 public key)
- hybrid PQ+classical key exchange (client combines ML-KEM-768 with x25519)
- future PQ TLS session key establishment
- Note: ML-KEM-768 addresses the harvest-now-decrypt-later threat for key agreement. See [`documents/SECURITY.md`](../documents/SECURITY.md) §16.1 for the PQ threat model.
- Verbs: `kem_encapsulate`, `kem_decapsulate` (see [`README.md`](../README.md) §5).
## 4. Curve and derivation notes
`n_signer` currently models curve metadata from day one:
`n_signer` models these curves:
- `secp256k1`
- `ed25519`
- `x25519`
- `secp256k1` — Nostr, Bitcoin
- `ed25519` — SSH signatures
- `x25519` — key agreement (age)
- `ml-dsa-65` — PQ signatures (FIPS 204)
- `slh-dsa-128s` — PQ hash-based signatures (FIPS 205)
- `ml-kem-768` — PQ key encapsulation (FIPS 203)
Not every purpose should be valid on every curve. Enforcement should be explicit in policy/runtime validation.
Derivation paths use BIP-44 structure with distinct coin types per algorithm family:
| Algorithm | Coin type | Path | Derivation style |
|---|---|---|---|
| secp256k1 (nostr) | 1237 | `m/44'/1237'/<n>'/0/0` | BIP-32 (NIP-06) |
| secp256k1 (bitcoin) | 0 | `m/44'/0'/<account>'/0/0` etc. | BIP-44 |
| ed25519 (ssh) | 102001 | `m/44'/102001'/<n>'/0'/0'` | SLIP-0010 (all hardened) |
| x25519 (age) | 102002 | `m/44'/102002'/<n>'/0'/0'` | SLIP-0010 (all hardened) |
| ML-DSA-65 (pq-sig) | 102003 | `m/44'/102003'/<n>'/0'/0'` | SLIP-0010 → seed → DRBG → PQClean keygen |
| SLH-DSA-128s (pq-sig) | 102004 | `m/44'/102004'/<n>'/0'/0'` | SLIP-0010 → seed → DRBG → PQClean keygen |
| ML-KEM-768 (pq-kem) | 102005 | `m/44'/102005'/<n>'/0'/0'` | SLIP-0010 → seed → DRBG → PQClean keygen |
The 102XXX coin type range is unregistered in SLIP-44 and chosen to avoid collisions with real cryptocurrencies. All non-secp256k1 paths are fully hardened per SLIP-0010. PQ keys use a seed→DRBG→PQClean keygen approach because PQ private keys are not scalars — see [`documents/SECURITY.md`](../documents/SECURITY.md) §16.2 for the security argument.
Not every purpose should be valid on every curve. Enforcement is explicit in the `(verb, purpose, curve)` matrix — see [`README.md`](../README.md) §6.
## 5. Operational caveats

263
plans/ssh_agent_proxy.md Normal file
View File

@@ -0,0 +1,263 @@
# Plan: SSH Agent Proxy Bridge for n_signer
## 1. Goal
Allow untrusted Qubes qubes to SSH into remote servers using an ed25519 private key held safely in n_signer, without the private key ever leaving the signer qube.
The untrusted qube runs a small proxy program that implements the OpenSSH `ssh-agent` protocol. OpenSSH talks to the proxy as if it were a normal `ssh-agent`. The proxy forwards signing requests to n_signer via qrexec. The private key never touches the untrusted qube's memory.
## 2. Architecture
```mermaid
graph LR
SSH[ssh client in untrusted qube] -->|SSH_AUTH_SOCK| PROXY[nsigner-ssh-agent proxy]
PROXY -->|qrexec qubes.NsignerRpc| SIGNER[n_signer in nostr_signer qube]
SIGNER -->|derives ed25519 key from mnemonic| KEY[ed25519 private key in mlock'd RAM]
SSH -->|SSH protocol| SERVER[remote SSH server]
```
**Flow:**
1. User in untrusted qube runs `ssh user@server`
2. OpenSSH connects to the proxy via `SSH_AUTH_SOCK`
3. OpenSSH sends `SSH_AGENTC_REQUEST_IDENTITIES` — proxy fetches the ed25519 public key from n_signer and returns it
4. OpenSSH sends the public key to the remote server as part of `SSH_MSG_USERAUTH_REQUEST`
5. The server sends back a challenge (the data to sign)
6. OpenSSH sends `SSH_AGENTC_SIGN_REQUEST` with the challenge data to the proxy
7. The proxy forwards the data to n_signer via qrexec: `{"method":"sign","params":["<hex>","algorithm":"ed25519","index":0]}`
8. n_signer signs the data with the ed25519 private key and returns the signature
9. The proxy returns the signature to OpenSSH
10. OpenSSH sends the signed authentication response to the server
**Key security property:** The private key exists only in n_signer's mlock'd memory in the `nostr_signer` qube. The untrusted qube never sees it. The proxy only ever handles public keys and signing requests/responses.
## 3. The ssh-agent protocol
OpenSSH's agent protocol is defined in `draft-miller-ssh-agent` (PROTOCOL.agent in the OpenSSH source). It uses a Unix socket with a simple message framing:
**Message format:**
```
uint32 message_length
byte message_type
byte[] message_data
```
**Messages the proxy must handle:**
### SSH_AGENTC_REQUEST_IDENTITIES (11)
Request the list of keys the agent holds.
**Response: SSH_AGENT_IDENTITIES_ANSWER (12)**
```
uint32 num_keys
string key_blob_1 (public key in SSH wire format)
string key_comment_1 (human-readable comment)
string key_blob_2
string key_comment_2
...
```
The proxy returns one key: the ed25519 public key from n_signer, formatted as an SSH ed25519 public key blob.
**SSH ed25519 public key blob format:**
```
string "ssh-ed25519"
string <32-byte public key>
```
### SSH_AGENTC_SIGN_REQUEST (13)
```
string key_blob (the public key to sign with)
string data (the data to sign)
uint32 flags (SSH_AGENT_SIGN_FLAG_* — currently 0 or SSH_AGENT_FLAG_RSA_SHA2_256/512 for RSA only)
```
**Response: SSH_AGENT_SIGN_RESPONSE (14)**
```
string signature_blob
```
**SSH ed25519 signature blob format:**
```
string "ssh-ed25519"
string <64-byte signature>
```
### Other messages
- `SSH_AGENTC_REMOVE_ALL_IDENTITIES` (11) — return `SSH_AGENT_SUCCESS`
- `SSH_AGENTC_REMOVE_IDENTITY` (18) — return `SSH_AGENT_SUCCESS`
- `SSH_AGENTC_LOCK` / `SSH_AGENTC_UNLOCK` (22/23) — return `SSH_AGENT_SUCCESS`
- `SSH_AGENTC_ADD_IDENTITY` (17) — return `SSH_AGENT_FAILURE` (we don't allow adding keys)
- Any unknown message — return `SSH_AGENT_FAILURE` (5)
## 4. Implementation
### 4.1 New program: `nsigner-ssh-agent`
A small C program (~400-500 lines) that:
1. Creates a Unix socket at a configurable path (default: `$XDG_RUNTIME_DIR/nsigner-ssh-agent.sock`)
2. Listens for connections from OpenSSH
3. On `SSH_AGENTC_REQUEST_IDENTITIES`:
- Calls n_signer via qrexec to get the ed25519 public key: `{"method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}`
- Parses the structured response to extract the 32-byte public key
- Formats it as an SSH ed25519 key blob
- Returns `SSH_AGENT_IDENTITIES_ANSWER` with one key
4. On `SSH_AGENTC_SIGN_REQUEST`:
- Extracts the data to sign from the request
- Calls n_signer via qrexec: `{"method":"sign","params":["<data_hex>",{"algorithm":"ed25519","index":0}]}`
- Parses the response to extract the 64-byte signature
- Formats it as an SSH ed25519 signature blob
- Returns `SSH_AGENT_SIGN_RESPONSE`
5. On other messages: returns appropriate responses (see §3)
**Key caching:** The proxy caches the public key after the first `REQUEST_IDENTITIES` call (it doesn't change during a session). The private key is never cached — each sign request goes to n_signer.
**Multiple keys:** The proxy can support multiple ed25519 keys by using different `index` values. Configuration via command-line args or environment variables:
```bash
nsigner-ssh-agent --index 0 --index 1 --index 2
```
Each index produces a different ed25519 key, all derived from the same mnemonic.
### 4.2 qrexec communication
The proxy communicates with n_signer via `qrexec-client-vm nostr_signer qubes.NsignerRpc`, using the same 4-byte big-endian length framing as the existing qrexec examples.
Each qrexec call is a one-shot: spawn `qrexec-client-vm`, send one framed request, receive one framed response, exit. This matches the existing qrexec protocol.
### 4.3 Configuration
**In the untrusted qube:**
```bash
# Start the proxy in the background
nsigner-ssh-agent --socket $XDG_RUNTIME_DIR/nsigner-ssh-agent.sock &
export SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/nsigner-ssh-agent.sock
export SSH_AUTH_SIGNER_QUBE=nostr_signer
# Now use ssh normally
ssh user@server
```
**In the nostr_signer qube:**
- n_signer must be running with the mnemonic loaded
- The `qubes.NsignerRpc` service must be installed
- The qrexec policy must allow the untrusted qube to call `qubes.NsignerRpc`
**Qubes policy (`packaging/qubes/policy.d/40-nsigner.policy`):**
```
nsigner.NsignerRpc +untrusted-qube nostr_signer allow
```
Or with the deny-by-default model from n_signer's policy:
```
nsigner.NsignerRpc +untrusted-qube nostr_signer ask
```
The `ask` policy shows a Qubes dom0 prompt each time the untrusted qube tries to sign, giving the user a chance to approve/deny.
### 4.4 n_signer policy
Use n_signer's `--preapprove` to pre-approve the untrusted qube for ed25519 signing:
```bash
nsigner --preapprove caller=qubes:untrusted-qube,algorithm=ed25519,index=0,verb=sign,get_public_key
```
Or use the deny-by-default prompt model — each sign request shows a prompt in the signer qube's TUI:
```
Caller: qubes:untrusted-qube
Action: sign with ed25519
Key: index 0 (key_id: 1fd9c73a93189484)
[a] approve [d] deny
```
### 4.5 File structure
```
src/nsigner_ssh_agent.c — the proxy program
packaging/qubes/
install-ssh-agent.sh — install the proxy in an AppVM
ssh-agent.desktop — autostart the proxy on qube boot
```
### 4.6 Build
The proxy is a separate binary, not part of the main nsigner binary. It links against:
- cJSON (for JSON-RPC parsing)
- libnostr_core (for the qrexec transport framing)
Makefile target:
```makefile
$(BUILD_DIR)/nsigner-ssh-agent: src/nsigner_ssh_agent.c
$(CC) $(CFLAGS) src/nsigner_ssh_agent.c -o $(BUILD_DIR)/nsigner-ssh-agent $(LDFLAGS)
```
## 5. Security considerations
### 5.1 The untrusted qube sees the public key
The ed25519 public key is sent to the untrusted qube. This is fine — public keys are not secret. The untrusted qube could share the public key with anyone, but that doesn't compromise the private key.
### 5.2 The untrusted qube controls what is signed
The untrusted qube constructs the SSH authentication message and sends it to the proxy for signing. The proxy forwards it to n_signer, which signs it without inspecting the content.
**Risk:** A compromised untrusted qube could ask n_signer to sign arbitrary data, not just SSH authentication messages. The ed25519 signature could be used for purposes other than SSH.
**Mitigation:** This is the same trust model as any ssh-agent. A compromised process with access to `SSH_AUTH_SOCK` can sign arbitrary data. The n_signer policy model (deny-by-default with per-request approval) provides an additional layer: the user can see each sign request at the signer's TUI and approve/deny it.
### 5.3 The proxy holds no secrets
The proxy process in the untrusted qube holds no private key material. It only has:
- The Unix socket path
- The qrexec target qube name
- The cached public key (non-secret)
- The ed25519 index to use
If the proxy process is compromised, the attacker gains the ability to forward sign requests to n_signer — but n_signer's policy still gates each request.
### 5.4 Qubes qrexec authentication
The qrexec framework authenticates the source qube. n_signer sees the caller as `qubes:<source-vm>`. This means:
- The signer knows which qube is requesting the signature
- The Qubes dom0 policy controls which qubes can call the service
- The n_signer policy can approve specific qubes for specific algorithms/verbs
### 5.5 No private key on disk
The ed25519 private key is never written to disk. It's derived from the mnemonic in n_signer's mlock'd RAM on each startup. The proxy doesn't have the mnemonic. The untrusted qube doesn't have the mnemonic. Only the `nostr_signer` qube has the mnemonic (entered at startup, never persisted).
## 6. What is explicitly out of scope
1. **RSA key support** — the proxy only supports ed25519. RSA SSH keys require different signing (PKCS#1 v1.5 or PSS) and different key derivation. ed25519 is the modern default.
2. **SSH certificate support** — OpenSSH certificates (ssh-ed25519-cert-v01@openssh.com) are not supported. These require a CA key to sign the certificate, which is a separate workflow.
3. **Agent forwarding**`ssh -A` (forwarding the agent to a remote host) is not supported. The proxy only accepts local Unix socket connections.
4. **Multiple signer qubes** — the proxy talks to one n_signer instance. Multiple signers would require multiple proxy instances.
5. **Hybrid PQ SSH keys** — OpenSSH doesn't support PQ signing keys yet. When it does, the proxy can be extended to use ML-DSA-65 via n_signer's `sign` verb with `algorithm=ml-dsa-65`.
## 7. Implementation phases
### Phase 1: Core proxy
- Implement `nsigner-ssh-agent.c` with ssh-agent protocol handling
- Implement qrexec communication with n_signer
- Support `SSH_AGENTC_REQUEST_IDENTITIES` and `SSH_AGENTC_SIGN_REQUEST`
- Test with a real SSH connection
### Phase 2: Qubes integration
- Install script for AppVMs
- Autostart on qube boot
- Qubes policy documentation
- n_signer `--preapprove` documentation
### Phase 3: Hardening
- Connection rate limiting (prevent sign-request flooding)
- Optional: inspect the sign request to verify it looks like an SSH auth message
- Optional: support multiple ed25519 indices (multiple SSH identities)
- Optional: support `SSH_AGENTC_REQUEST_EXTENSION` for OpenSSH extensions
## 8. Open questions
1. **Should the proxy inspect the sign request data?** The proxy could check that the data being signed looks like an SSH authentication message (starts with the session ID, has the right message type). This would prevent the untrusted qube from using the signer for non-SSH signing. However, it's fragile (SSH protocol may change) and doesn't match how normal ssh-agents work. Default: no inspection, rely on n_signer's policy.
2. **Should the proxy support `ssh-add -l`?** `ssh-add -l` lists the keys the agent holds, which maps to `SSH_AGENTC_REQUEST_IDENTITIES`. Yes, this is already supported by the protocol handling.
3. **Should the proxy cache the public key across connections?** Yes — the public key doesn't change during a session. Cache it after the first `REQUEST_IDENTITIES` call. Clear the cache on `SIGHUP` or when the qrexec call fails.
4. **Should we support `sk-ssh-ed25519@openssh.com` (security key) key type?** This is the YubiKey key type. It adds a "flags" byte to the signature (e.g., "require user presence"). Not needed for n_signer, but could be useful for compatibility with systems that expect security key signatures. Default: no, use standard `ssh-ed25519`.

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -203,6 +226,12 @@ const char *selector_strerror(int err);
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
/*
* Check whether `verb` is allowed to execute against `role`.
@@ -230,6 +259,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +285,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +324,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +345,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +516,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +600,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -455,28 +715,181 @@ static int is_nostr_verb(const char *verb) {
return 0;
}
/* get_public_key is a universal verb — allowed for all algorithms.
* It is handled separately in enforce_verb_role() and excluded here. */
return (strcmp(verb, VERB_SIGN_EVENT) == 0) ||
(strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) ||
(strcmp(verb, VERB_NIP44_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NIP44_DECRYPT) == 0) ||
(strcmp(verb, VERB_NIP04_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NIP04_DECRYPT) == 0);
(strcmp(verb, VERB_NIP04_DECRYPT) == 0) ||
(strcmp(verb, VERB_MINE_EVENT) == 0);
}
static int is_sign_data_verb(const char *verb) {
if (verb == NULL) {
return 0;
}
return (strcmp(verb, VERB_SIGN_DATA) == 0) ||
(strcmp(verb, VERB_VERIFY_SIG) == 0);
}
static int is_kem_verb(const char *verb) {
if (verb == NULL) {
return 0;
}
return (strcmp(verb, VERB_KEM_ENCAPS) == 0) ||
(strcmp(verb, VERB_KEM_DECAPS) == 0);
}
/* Check whether a (purpose, curve) pair is allowed for sign_data / verify_signature.
* Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */
static int enforce_sign_data_role(const role_entry_t *role) {
switch (role->purpose) {
case PURPOSE_SSH:
if (role->curve == CURVE_ED25519) {
return ENFORCE_OK;
}
return ENFORCE_ERR_CURVE;
case PURPOSE_PQ_SIG:
if (role->curve == CURVE_ML_DSA_65 ||
role->curve == CURVE_SLH_DSA_128S) {
return ENFORCE_OK;
}
return ENFORCE_ERR_CURVE;
default:
return ENFORCE_ERR_PURPOSE;
}
}
/* Check whether a (purpose, curve) pair is allowed for kem_encapsulate/decapsulate.
* Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */
static int enforce_kem_role(const role_entry_t *role) {
if (role->purpose != PURPOSE_PQ_KEM) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_ML_KEM_768) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
}
int enforce_verb_role(const char *verb, const role_entry_t *role) {
if (!is_nostr_verb(verb)) {
if (verb == NULL || role == NULL) {
return ENFORCE_ERR_UNKNOWN_VERB;
}
if (role->purpose != PURPOSE_NOSTR) {
return ENFORCE_ERR_PURPOSE;
/* get_public_key is a universal verb — allowed for any role whose
* (curve, purpose) maps to a known algorithm via crypto_alg_from_role().
* This lets clients retrieve public keys for all 6 algorithms. */
if (strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) {
crypto_alg_t alg = crypto_alg_from_role(role->curve, role->purpose);
if (alg == CRYPTO_ALG_UNKNOWN) {
return ENFORCE_ERR_PURPOSE;
}
return ENFORCE_OK;
}
if (role->curve != CURVE_SECP256K1) {
return ENFORCE_ERR_CURVE;
/* Nostr verbs: PURPOSE_NOSTR + CURVE_SECP256K1 only. */
if (is_nostr_verb(verb)) {
if (role->purpose != PURPOSE_NOSTR) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_SECP256K1) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
}
return ENFORCE_OK;
/* ssh_sign: PURPOSE_SSH + CURVE_ED25519 only. */
if (strcmp(verb, VERB_SSH_SIGN) == 0) {
if (role->purpose != PURPOSE_SSH) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_ED25519) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
}
/* sign_data / verify_signature: SSH+ed25519 or PQ-SIG algorithms. */
if (is_sign_data_verb(verb)) {
return enforce_sign_data_role(role);
}
/* kem_encapsulate / kem_decapsulate: PQ-KEM + ML-KEM-768 only. */
if (is_kem_verb(verb)) {
return enforce_kem_role(role);
}
return ENFORCE_ERR_UNKNOWN_VERB;
}
/* ---- Algorithm-based enforcement (new) ----
*
* enforce_verb_algorithm() checks whether a verb is valid for a given
* algorithm, WITHOUT consulting the role table or purpose field. This is
* the enforcement path for the new algorithm-based verbs (sign, verify,
* encapsulate, decapsulate, derive_shared_secret, get_public_key).
*
* The Nostr-specific verbs (sign_event, nip44_*, nip04_*, mine_event) are
* always secp256k1 — that's a Nostr protocol requirement, not a policy
* choice. They are accepted here only when alg == CRYPTO_ALG_SECP256K1
* so that algorithm-based policy entries can approve them too.
*/
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) {
if (verb == NULL) {
return ENFORCE_ERR_UNKNOWN_VERB;
}
/* get_public_key: valid for all known algorithms. */
if (strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) {
if (alg == CRYPTO_ALG_UNKNOWN) {
return ENFORCE_ERR_ALGORITHM;
}
return ENFORCE_OK;
}
/* sign / verify: signature algorithms only. */
if (strcmp(verb, VERB_SIGN) == 0 || strcmp(verb, VERB_VERIFY) == 0) {
if (alg == CRYPTO_ALG_SECP256K1 ||
alg == CRYPTO_ALG_ED25519 ||
alg == CRYPTO_ALG_ML_DSA_65 ||
alg == CRYPTO_ALG_SLH_DSA_128S) {
return ENFORCE_OK;
}
return ENFORCE_ERR_ALGORITHM;
}
/* encapsulate / decapsulate: ML-KEM-768 only. */
if (strcmp(verb, VERB_ENCAPSULATE) == 0 || strcmp(verb, VERB_DECAPSULATE) == 0) {
if (alg == CRYPTO_ALG_ML_KEM_768) {
return ENFORCE_OK;
}
return ENFORCE_ERR_ALGORITHM;
}
/* derive_shared_secret: x25519 only. */
if (strcmp(verb, VERB_DERIVE_SHARED) == 0) {
if (alg == CRYPTO_ALG_X25519) {
return ENFORCE_OK;
}
return ENFORCE_ERR_ALGORITHM;
}
/* Nostr verbs: always secp256k1 (protocol requirement). */
if (is_nostr_verb(verb) || strcmp(verb, VERB_MINE_EVENT) == 0) {
if (alg == CRYPTO_ALG_SECP256K1) {
return ENFORCE_OK;
}
return ENFORCE_ERR_ALGORITHM;
}
/* Old verb aliases map to the new verbs and are handled by the
* dispatcher before reaching enforcement. If they reach here,
* treat them as unknown (the dispatcher should have remapped). */
return ENFORCE_ERR_UNKNOWN_VERB;
}
const char *enforce_strerror(int err) {
@@ -489,6 +902,8 @@ const char *enforce_strerror(int err) {
return "curve_mismatch";
case ENFORCE_ERR_UNKNOWN_VERB:
return "unknown_verb";
case ENFORCE_ERR_ALGORITHM:
return "algorithm_not_supported_for_verb";
default:
return "unknown_error";
}

File diff suppressed because it is too large Load Diff

View File

@@ -84,6 +84,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -92,6 +94,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -200,6 +205,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -235,6 +258,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -259,6 +284,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -300,6 +331,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -307,15 +352,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -346,6 +523,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -354,10 +607,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -412,6 +666,9 @@ typedef struct {
} caller_identity_t;
/* Server context */
#define INDEX_WHITELIST_MAX 256
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8)
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
char last_error[256];
@@ -424,6 +681,9 @@ typedef struct {
int socket_name_explicit;
int auth_mode;
int auth_skew_seconds;
int bridge_source_trusted;
int index_whitelist_active;
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE];
} server_ctx_t;
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
@@ -457,6 +717,9 @@ void server_set_prompt_always_allow(int enabled);
/* Mark the unix listener as a trusted bridge socket (qrexec source preamble) */
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
/* Set the nostr_index whitelist from a spec string ("all", "1,3,4", "0-3", "0-3,7,9") */
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
/* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */
void server_set_noninteractive_prompt_default(int decision);
@@ -494,8 +757,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 37
#define NSIGNER_VERSION "v0.0.37"
#define NSIGNER_VERSION_PATCH 47
#define NSIGNER_VERSION "v0.0.47"
/* NSIGNER_HEADERLESS_DECLS_END */
@@ -527,6 +790,7 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
#include <unistd.h>
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
#define NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc"
#define ACTIVITY_LOG_CAP 16
#define CONNECTION_INFO_CAP 8
@@ -555,10 +819,10 @@ static char g_connection_info[CONNECTION_INFO_CAP][192];
static int g_connection_info_count = 0;
static const TuiMenuItem g_main_menu_items[] = {
{"^_q^:/x quit", 'q'},
{"^_l^: lock/reunlock", 'l'},
{"^_r^: refresh", 'r'},
{"^_A^: toggle auto-approve", 'a'}
{"^_a^: toggle auto-approve", 'a'},
{"^_q^:/x quit", 'q'}
};
typedef struct {
@@ -799,6 +1063,8 @@ static void print_usage(const char *program_name) {
tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup");
tui_print(" --allow-all, -A Allow all policy prompts for this server session");
tui_print(" --bridge-source-trusted Accept qrexec_source preamble on unix connections (bridge mode)");
tui_print(" --allow-index SPEC Restrict which nostr_index values this session can access");
tui_print(" SPEC: 'all' (default), '1,3,4', '0-3', or '0-3,7,9'");
}
static int extract_nsigner_socket_from_proc_line(const char *line,
@@ -1089,7 +1355,7 @@ static void activity_log_cb(const char *message, void *user_data) {
int idx;
time_t now;
struct tm tm_now;
char ts[16];
char ts[32];
(void)user_data;
if (message == NULL) {
@@ -1098,9 +1364,9 @@ static void activity_log_cb(const char *message, void *user_data) {
now = time(NULL);
if (localtime_r(&now, &tm_now) != NULL) {
(void)strftime(ts, sizeof(ts), "%m%d-%H%M%S", &tm_now);
(void)strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now);
} else {
strncpy(ts, "0000-000000", sizeof(ts) - 1);
strncpy(ts, "0000-00-00 00:00:00", sizeof(ts) - 1);
ts[sizeof(ts) - 1] = '\0';
}
@@ -1171,7 +1437,7 @@ static void render_status(const role_table_t *role_table,
TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Main Menu" };
TuiMenu menu = { g_main_menu_items, (int)(sizeof(g_main_menu_items) / sizeof(g_main_menu_items[0])) };
TuiSize size = tui_terminal_size();
int left_col = tui_menu_left_col(&frame, size.width);
const int left_col = 0;
char status_buf[256];
tui_clear_continuous(size.height);
@@ -1187,19 +1453,6 @@ static void render_status(const role_table_t *role_table,
}
tui_print("");
tui_render_menu(&menu, left_col);
tui_print("");
(void)snprintf(status_buf,
sizeof(status_buf),
"session=%s (%d words) signer=%s derived=%d auto-approve=%s",
mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked",
(mnemonic != NULL) ? mnemonic->word_count : 0,
(socket_name != NULL) ? socket_name : "(none)",
derived_count,
g_auto_approve ? "ON" : "OFF");
tui_print("%s", status_buf);
tui_print("^*Roles^:");
if (role_table == NULL || role_table->count == 0) {
tui_print("(none)");
@@ -1236,6 +1489,19 @@ static void render_status(const role_table_t *role_table,
}
}
tui_print("");
(void)snprintf(status_buf,
sizeof(status_buf),
"session=%s (%d words) signer=%s derived=%d auto-approve=%s",
mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked",
(mnemonic != NULL) ? mnemonic->word_count : 0,
(socket_name != NULL) ? socket_name : "(none)",
derived_count,
g_auto_approve ? "ON" : "OFF");
tui_print("%s", status_buf);
tui_print("");
tui_render_menu(&menu, left_col);
tui_anchor_prompt(0, left_col);
fflush(stdout);
}
@@ -1582,7 +1848,7 @@ static void apply_test_overrides(policy_table_t *policy) {
const char *p = hotkeys;
while (*p != '\0') {
char ch = *p;
if (ch == 'A') {
if (ch == 'a') {
g_auto_approve = g_auto_approve ? 0 : 1;
server_set_prompt_always_allow(g_auto_approve);
}
@@ -1615,9 +1881,7 @@ static int prompt_transport_selection(void) {
(selected & TRANSPORT_QREXEC_BRIDGE) ? "x" : " ");
printf(" [%s] 3. TCP listener (FIPS mesh or local network)\n",
(selected & TRANSPORT_TCP) ? "x" : " ");
printf(" [%s] 4. Qrexec one-shot (legacy, one request per invocation)\n",
(selected & TRANSPORT_QREXEC_ONESHOT) ? "x" : " ");
printf("\n [a] select all (except 4) Enter = confirm\n");
printf("\n [a] select all Enter = confirm\n");
printf("> ");
fflush(stdout);
@@ -1640,15 +1904,6 @@ static int prompt_transport_selection(void) {
printf("At least one transport must be selected.\n");
continue;
}
/* Qrexec one-shot is mutually exclusive with persistent listeners */
if ((selected & TRANSPORT_QREXEC_ONESHOT) && (selected & ~TRANSPORT_QREXEC_ONESHOT)) {
printf("Qrexec one-shot cannot combine with other transports. Deselecting it.\n");
selected &= ~TRANSPORT_QREXEC_ONESHOT;
if (selected == 0) {
selected = TRANSPORT_UNIX;
}
continue;
}
break;
}
@@ -1663,16 +1918,67 @@ static int prompt_transport_selection(void) {
selected ^= TRANSPORT_QREXEC_BRIDGE;
} else if (input[0] == '3') {
selected ^= TRANSPORT_TCP;
} else if (input[0] == '4') {
selected ^= TRANSPORT_QREXEC_ONESHOT;
} else {
printf("Invalid input. Type 1-4, 'a', or Enter to confirm.\n");
printf("Invalid input. Type 1-3, 'a', or Enter to confirm.\n");
}
}
return selected;
}
/*
* Interactive index whitelist prompt.
* Shown after transport selection when no --allow-index flag was given.
* Returns a malloc'd spec string (caller must free), or NULL for "all".
*/
static char *prompt_index_whitelist(void) {
char input[256];
for (;;) {
tui_render_content_screen(NULL, "Index whitelist — restrict which nostr_index values this session can access");
printf("Enter allowed indices, or press Enter for 'all' (no restriction):\n\n");
printf(" Examples:\n");
printf(" all (default — allow all indices)\n");
printf(" 0 (only index 0)\n");
printf(" 0,1,3 (specific indices)\n");
printf(" 0-3 (range 0 through 3)\n");
printf(" 0,2-3,7 (mixed list and ranges)\n");
printf("\n Enter = all\n");
printf("> ");
fflush(stdout);
if (read_line_stdin(input, sizeof(input)) != 0) {
return NULL;
}
/* Trim trailing whitespace */
{
size_t len = strlen(input);
while (len > 0 && (input[len-1] == '\n' || input[len-1] == '\r' ||
input[len-1] == ' ' || input[len-1] == '\t')) {
input[--len] = '\0';
}
}
if (input[0] == '\0') {
/* Enter pressed — default to "all" */
return NULL;
}
/* Validate by parsing into a temp whitelist */
{
server_ctx_t tmp;
memset(&tmp, 0, sizeof(tmp));
if (server_set_index_whitelist(&tmp, input) != 0) {
printf("Invalid spec: '%s'. Try again or press Enter for 'all'.\n", input);
continue;
}
}
return strdup(input);
}
}
int main(int argc, char *argv[]) {
mnemonic_state_t mnemonic;
role_table_t role_table;
@@ -1680,6 +1986,7 @@ int main(int argc, char *argv[]) {
policy_table_t policy;
server_ctx_t server;
key_store_t key_store;
static algorithm_key_cache_t alg_key_cache;
struct pollfd pfds[3]; /* max: 2 listeners (unix+tcp) + stdin */
uid_t owner_uid;
int derived_count;
@@ -1696,6 +2003,7 @@ int main(int argc, char *argv[]) {
int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
int allow_all = 0;
int bridge_source_trusted = 0;
const char *allow_index_spec = NULL;
mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 };
while (argi < argc) {
@@ -1806,6 +2114,15 @@ int main(int argc, char *argv[]) {
argi += 1;
continue;
}
if (strcmp(argv[argi], "--allow-index") == 0) {
if (argi + 1 >= argc) {
fprintf(stderr, "Missing value for %s\n", argv[argi]);
return 1;
}
allow_index_spec = argv[argi + 1];
argi += 2;
continue;
}
break;
}
@@ -1882,6 +2199,7 @@ int main(int argc, char *argv[]) {
}
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&alg_key_cache);
owner_uid = getuid();
policy_init_default(&policy, owner_uid);
@@ -1897,7 +2215,10 @@ int main(int argc, char *argv[]) {
entry.source = POLICY_SOURCE_PREAPPROVE;
if (parsed_nostr_index >= 0 && role_table_find_by_nostr_index(&role_table, parsed_nostr_index) == NULL) {
/* Role-based entries: register the nostr_index role if needed.
* Algorithm-based entries (alg_count > 0) don't need a role. */
if (entry.alg_count == 0 && parsed_nostr_index >= 0 &&
role_table_find_by_nostr_index(&role_table, parsed_nostr_index) == NULL) {
if (role_table_register_nostr_index(&role_table, parsed_nostr_index) != 0) {
fprintf(stderr,
"ERROR: failed to register role for --preapprove nostr_index=%d\n",
@@ -1913,7 +2234,12 @@ int main(int argc, char *argv[]) {
return 1;
}
fprintf(stderr, "[PREAPPROVE] caller=%s role=%s\n", entry.caller, entry.roles[0]);
if (entry.alg_count > 0) {
fprintf(stderr, "[PREAPPROVE] caller=%s algorithm=%s verbs=%d\n",
entry.caller, entry.algorithms[0], entry.verb_count);
} else {
fprintf(stderr, "[PREAPPROVE] caller=%s role=%s\n", entry.caller, entry.roles[0]);
}
}
apply_test_overrides(&policy);
@@ -1935,7 +2261,7 @@ int main(int argc, char *argv[]) {
return 1;
}
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store);
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store, &alg_key_cache);
/*
* Interactive transport selection: if no --listen flag was given and
@@ -1955,43 +2281,45 @@ int main(int argc, char *argv[]) {
transport_mask = prompt_transport_selection();
if (transport_mask == 0) {
fprintf(stderr, "No transport selected. Exiting.\n");
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
}
/* Qrexec one-shot: use existing single-listener path */
if (transport_mask & TRANSPORT_QREXEC_ONESHOT) {
listen_mode = NSIGNER_LISTEN_QREXEC;
listen_mode_explicit = 1;
} else {
/* Persistent listeners — configure from menu selection */
multi_listen = 1;
/* Unix socket (with or without bridge-source-trusted) */
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
/* Use fixed name "nsigner" for scripted access */
socket_name = "nsigner";
socket_name_explicit = 1;
listen_mode = NSIGNER_LISTEN_UNIX;
listen_target = socket_name;
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
bridge_source_trusted = 1;
}
/* Index whitelist prompt (only if --allow-index wasn't given on CLI) */
if (allow_index_spec == NULL) {
char *wl_spec = prompt_index_whitelist();
if (wl_spec != NULL) {
allow_index_spec = wl_spec; /* will be freed at program exit */
}
}
/* TCP listener */
if (transport_mask & TRANSPORT_TCP) {
/* Will be started as a second listener */
/* Persistent listeners — configure from menu selection */
multi_listen = 1;
/* Unix socket (with or without bridge-source-trusted) */
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
/* Use fixed name "nsigner" for scripted access */
socket_name = "nsigner";
socket_name_explicit = 1;
listen_mode = NSIGNER_LISTEN_UNIX;
listen_target = socket_name;
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
bridge_source_trusted = 1;
}
}
/* TCP listener */
if (transport_mask & TRANSPORT_TCP) {
/* Will be started as a second listener */
}
}
if (listen_mode == NSIGNER_LISTEN_UNIX && !socket_name_explicit) {
if (socket_name_random(generated_socket_name, sizeof(generated_socket_name)) != 0) {
fprintf(stderr, "Failed to generate random socket name\n");
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
@@ -2003,7 +2331,7 @@ int main(int argc, char *argv[]) {
listen_target = socket_name;
} else if (listen_mode == NSIGNER_LISTEN_TCP && socket_name_explicit) {
fprintf(stderr, "--socket-name is only valid with unix listen mode\n");
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
@@ -2012,7 +2340,7 @@ int main(int argc, char *argv[]) {
if ((listen_mode == NSIGNER_LISTEN_UNIX || listen_mode == NSIGNER_LISTEN_STDIO) &&
auth_mode != NSIGNER_AUTH_OFF) {
fprintf(stderr, "--auth is currently supported only with --listen qrexec (optional/required) and tcp (required)\n");
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
@@ -2033,6 +2361,16 @@ int main(int argc, char *argv[]) {
if (bridge_source_trusted) {
server_set_bridge_source_trusted(&server, 1);
}
if (allow_index_spec != NULL) {
if (server_set_index_whitelist(&server, allow_index_spec) != 0) {
fprintf(stderr, "Invalid --allow-index spec: %s\n", allow_index_spec);
fprintf(stderr, "Expected: 'all', '1,3,4', '0-3', or '0-3,7,9'\n");
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
}
}
if (server_start(&server) != 0) {
if (listen_mode == NSIGNER_LISTEN_UNIX) {
fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server));
@@ -2043,7 +2381,7 @@ int main(int argc, char *argv[]) {
(listen_mode == NSIGNER_LISTEN_QREXEC) ? "qrexec" : "stdio",
server_last_error(&server));
}
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
@@ -2065,11 +2403,14 @@ int main(int argc, char *argv[]) {
auth_skew_seconds,
&dispatcher,
&policy);
if (allow_index_spec != NULL) {
server_set_index_whitelist(&servers[tcp_server_idx], allow_index_spec);
}
if (server_start(&servers[tcp_server_idx]) != 0) {
fprintf(stderr, "Failed to start TCP server on %s: %s\n",
tcp_target, server_last_error(&servers[tcp_server_idx]));
server_stop(&server);
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
@@ -2114,6 +2455,11 @@ int main(int argc, char *argv[]) {
connection_info_add("listen: unix @%s", socket_name);
}
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME);
connection_info_add("qrexec caller: qrexec-client-vm <target_qube> %s", NSIGNER_QREXEC_SERVICE_NAME);
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
}
printf("System is ready and waiting for connections on @%s.\n", socket_name);
}
if (transport_mask & TRANSPORT_TCP) {
@@ -2123,6 +2469,11 @@ int main(int argc, char *argv[]) {
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
connection_info_add("listen: unix @%s", socket_name);
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
if (bridge_source_trusted) {
connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME);
connection_info_add("qrexec caller: qrexec-client-vm <target_qube> %s", NSIGNER_QREXEC_SERVICE_NAME);
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
}
printf("System is ready and waiting for connections on @%s.\n", socket_name);
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
connection_info_add("listen: %s", listen_target);
@@ -2153,7 +2504,7 @@ int main(int argc, char *argv[]) {
if (listen_mode == NSIGNER_LISTEN_STDIO || listen_mode == NSIGNER_LISTEN_QREXEC) {
int hrc = server_handle_one(&server, NULL, NULL);
server_stop(&server);
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return (hrc < 0) ? 1 : 0;
@@ -2179,7 +2530,7 @@ int main(int argc, char *argv[]) {
}
server_stop(&server);
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 0;
@@ -2261,7 +2612,7 @@ int main(int argc, char *argv[]) {
g_running = 0;
} else if (lower == 'r') {
render_status(&role_table, &mnemonic, derived_count, socket_name);
} else if (ch == 'A') {
} else if (ch == 'a') {
g_auto_approve = g_auto_approve ? 0 : 1;
server_set_prompt_always_allow(g_auto_approve);
render_status(&role_table, &mnemonic, derived_count, socket_name);
@@ -2270,7 +2621,7 @@ int main(int argc, char *argv[]) {
printf("\n[lock] Session locked. Re-enter mnemonic to unlock.\n");
fflush(stdout);
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
mnemonic_unload(&mnemonic);
relock_source.kind = MNEMONIC_SOURCE_TUI;
@@ -2304,7 +2655,7 @@ int main(int argc, char *argv[]) {
} else {
server_stop(&server);
}
crypto_wipe(&key_store);
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
nostr_cleanup();
mnemonic_unload(&mnemonic);
printf("Shutdown. All secrets wiped.\n");

368
src/miner.c Normal file
View File

@@ -0,0 +1,368 @@
/*
* miner.c - Multithreaded best-effort NIP-13 Proof-of-Work mining coordinator
*
* This module implements a server-safe mining loop that:
* - Runs N worker threads, each trying nonces with a unique stride
* - Tracks the best event (highest difficulty) across all threads
* - Stops when target difficulty is reached OR timeout expires
* - Always returns the best result found (best-effort, not failure on timeout)
*
* Unlike the standalone event_miner tool, this code:
* - Has no global variables (all state in context structs)
* - Never calls exit() (returns result codes)
* - Has no signal handlers (the server handles signals)
* - Uses time-based termination as the primary budget
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>
#include <cJSON.h>
#include <nostr_core/nip001.h>
#include <nostr_core/nip013.h>
#include <nostr_core/utils.h>
/* ---- Constants ---- */
#define MINER_MAX_THREADS 32
#define MINER_SAFETY_TIMEOUT 600 /* 10 minutes safety max when only difficulty is set */
#define MINER_NONCE_STRIDE 0 /* 0 = use thread_count as stride */
/* ---- Result structure ---- */
typedef struct {
cJSON *best_event; /* best event found (caller frees) */
int achieved_difficulty;
int target_difficulty;
int target_reached;
int elapsed_sec;
uint64_t total_attempts;
} mine_result_t;
/* ---- Shared state across worker threads ---- */
typedef struct {
cJSON *best_event; /* best event found so far (mutex-protected) */
int best_difficulty; /* difficulty of best_event */
uint64_t total_attempts; /* total attempts across all threads */
int target_difficulty;/* 0 = no target, mine full timeout */
int target_reached; /* 1 if target was reached */
pthread_mutex_t mutex; /* protects best_event, best_difficulty, total_attempts */
volatile int stop; /* set when target reached or timeout */
} mine_shared_state_t;
/* ---- Per-worker context ---- */
typedef struct {
mine_shared_state_t *shared;
int kind;
char content[65536]; /* copy of event content */
cJSON *tags_template; /* this thread's copy of original tags */
unsigned char private_key[32];
int thread_id;
uint64_t nonce_start; /* starting nonce for this thread */
uint64_t nonce_stride; /* increment to avoid overlap with other threads */
time_t deadline; /* absolute time to stop */
uint64_t attempts; /* this thread's attempt count */
} miner_worker_ctx_t;
/* ---- Helper: update nonce tag in a tags array ---- */
static int miner_update_nonce_tag(cJSON *tags, uint64_t nonce, int target_difficulty) {
cJSON *tag;
int index = 0;
if (!tags) return -1;
/* Remove existing nonce tag if present */
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON *tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type) &&
strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
cJSON_DetachItemFromArray(tags, index);
cJSON_Delete(tag);
break;
}
}
index++;
}
/* Add new nonce tag: ["nonce", "<nonce>", "<target>"] */
{
cJSON *nonce_tag = cJSON_CreateArray();
char nonce_str[32];
char difficulty_str[16];
if (!nonce_tag) return -1;
snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));
cJSON_AddItemToArray(tags, nonce_tag);
}
return 0;
}
/* ---- Worker thread function ---- */
static void *miner_worker(void *arg) {
miner_worker_ctx_t *ctx = (miner_worker_ctx_t *)arg;
uint64_t nonce = ctx->nonce_start;
time_t current_ts = time(NULL);
while (!ctx->shared->stop && time(NULL) < ctx->deadline) {
cJSON *working_tags;
cJSON *signed_event;
const char *event_id;
int difficulty;
/* Build tags with current nonce */
working_tags = cJSON_Duplicate(ctx->tags_template, 1);
if (!working_tags) break;
if (miner_update_nonce_tag(working_tags, nonce, ctx->shared->target_difficulty) != 0) {
cJSON_Delete(working_tags);
break;
}
/* Update timestamp periodically (every 1000 attempts) to keep it fresh */
if (ctx->attempts % 1000 == 0) {
current_ts = time(NULL);
}
/* Create and sign the event with this nonce */
signed_event = nostr_create_and_sign_event(ctx->kind, ctx->content,
working_tags, ctx->private_key,
current_ts);
cJSON_Delete(working_tags);
if (!signed_event) {
nonce += ctx->nonce_stride;
ctx->attempts++;
continue;
}
/* Check difficulty of the resulting event id */
{
cJSON *id_item = cJSON_GetObjectItem(signed_event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
cJSON_Delete(signed_event);
nonce += ctx->nonce_stride;
ctx->attempts++;
continue;
}
event_id = cJSON_GetStringValue(id_item);
}
difficulty = nostr_calculate_pow_difficulty(event_id);
/* Track best under mutex */
pthread_mutex_lock(&ctx->shared->mutex);
ctx->shared->total_attempts++;
if (difficulty > ctx->shared->best_difficulty) {
if (ctx->shared->best_event) {
cJSON_Delete(ctx->shared->best_event);
}
ctx->shared->best_event = cJSON_Duplicate(signed_event, 1);
ctx->shared->best_difficulty = difficulty;
}
if (ctx->shared->target_difficulty > 0 && difficulty >= ctx->shared->target_difficulty) {
ctx->shared->target_reached = 1;
ctx->shared->stop = 1;
}
pthread_mutex_unlock(&ctx->shared->mutex);
cJSON_Delete(signed_event);
nonce += ctx->nonce_stride;
ctx->attempts++;
}
return NULL;
}
/* ---- Main mining coordinator ---- */
/*
* Run multithreaded best-effort mining on `event`.
*
* Parameters:
* event - cJSON event to mine (must have kind, content, tags, created_at)
* private_key - 32-byte private key for signing
* target_difficulty - 0 = no target (mine full timeout); >0 = stop when reached
* thread_count - number of worker threads (1..MINER_MAX_THREADS)
* timeout_sec - time budget in seconds (0 = use MINER_SAFETY_TIMEOUT)
* result - output struct (caller frees result->best_event)
*
* Returns 0 on success (result populated), -1 on error.
*/
int miner_run(cJSON *event, const unsigned char *private_key,
int target_difficulty, int thread_count, int timeout_sec,
mine_result_t *result)
{
mine_shared_state_t shared;
miner_worker_ctx_t *workers = NULL;
pthread_t *threads = NULL;
cJSON *kind_item, *content_item, *tags_item, *created_at_item;
int kind;
const char *content;
time_t start_time;
int effective_timeout;
int i;
int ret = 0;
if (!event || !private_key || !result) {
return -1;
}
memset(result, 0, sizeof(mine_result_t));
/* Validate event has required fields */
kind_item = cJSON_GetObjectItem(event, "kind");
content_item = cJSON_GetObjectItem(event, "content");
tags_item = cJSON_GetObjectItem(event, "tags");
created_at_item = cJSON_GetObjectItem(event, "created_at");
if (!kind_item || !content_item || !tags_item) {
return -1;
}
/* If created_at is missing, add it with current time */
if (!created_at_item) {
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
}
kind = (int)cJSON_GetNumberValue(kind_item);
content = cJSON_GetStringValue(content_item);
if (!content) {
content = "";
}
/* Clamp thread count */
if (thread_count < 1) thread_count = 1;
if (thread_count > MINER_MAX_THREADS) thread_count = MINER_MAX_THREADS;
/* Determine effective timeout */
if (timeout_sec <= 0) {
effective_timeout = MINER_SAFETY_TIMEOUT;
} else {
effective_timeout = timeout_sec;
}
/* Initialize shared state */
memset(&shared, 0, sizeof(shared));
shared.best_event = NULL;
shared.best_difficulty = -1;
shared.total_attempts = 0;
shared.target_difficulty = target_difficulty;
shared.target_reached = 0;
shared.stop = 0;
if (pthread_mutex_init(&shared.mutex, NULL) != 0) {
return -1;
}
/* Allocate worker contexts and thread array */
workers = (miner_worker_ctx_t *)calloc(thread_count, sizeof(miner_worker_ctx_t));
threads = (pthread_t *)calloc(thread_count, sizeof(pthread_t));
if (!workers || !threads) {
free(workers);
free(threads);
pthread_mutex_destroy(&shared.mutex);
return -1;
}
start_time = time(NULL);
/* Set up each worker */
for (i = 0; i < thread_count; i++) {
workers[i].shared = &shared;
workers[i].kind = kind;
strncpy(workers[i].content, content, sizeof(workers[i].content) - 1);
workers[i].content[sizeof(workers[i].content) - 1] = '\0';
workers[i].tags_template = cJSON_Duplicate(tags_item, 1);
memcpy(workers[i].private_key, private_key, 32);
workers[i].thread_id = i;
workers[i].nonce_start = (uint64_t)i;
workers[i].nonce_stride = (uint64_t)thread_count;
workers[i].deadline = start_time + effective_timeout;
workers[i].attempts = 0;
if (!workers[i].tags_template) {
/* Allocation failed for this worker's tags — clean up what we have */
int j;
for (j = 0; j < i; j++) {
cJSON_Delete(workers[j].tags_template);
}
free(workers);
free(threads);
pthread_mutex_destroy(&shared.mutex);
return -1;
}
}
/* Start threads */
for (i = 0; i < thread_count; i++) {
if (pthread_create(&threads[i], NULL, miner_worker, &workers[i]) != 0) {
/* Failed to create thread i — signal stop and join what we have */
shared.stop = 1;
{
int j;
for (j = 0; j < i; j++) {
pthread_join(threads[j], NULL);
}
}
ret = -1;
goto cleanup;
}
}
/* Wait for all threads to complete (they stop on target/timeout) */
for (i = 0; i < thread_count; i++) {
pthread_join(threads[i], NULL);
}
cleanup:
/* Sum up total attempts from all workers */
{
uint64_t total = 0;
for (i = 0; i < thread_count; i++) {
total += workers[i].attempts;
}
/* Use the shared counter (more accurate, includes mutex-protected increments) */
result->total_attempts = shared.total_attempts > 0 ? shared.total_attempts : total;
}
/* Populate result */
result->best_event = shared.best_event; /* transfer ownership */
result->achieved_difficulty = shared.best_difficulty;
result->target_difficulty = target_difficulty;
result->target_reached = shared.target_reached;
result->elapsed_sec = (int)(time(NULL) - start_time);
/* Clean up worker contexts */
for (i = 0; i < thread_count; i++) {
if (workers[i].tags_template) {
cJSON_Delete(workers[i].tags_template);
}
/* Zeroize private key copy */
memset(workers[i].private_key, 0, 32);
}
free(workers);
free(threads);
pthread_mutex_destroy(&shared.mutex);
return ret;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -515,6 +769,22 @@ static int string_in_purposes(const char *needle, char haystack[][ROLE_PURPOSE_M
return 0;
}
static int string_in_algorithms(const char *needle, char haystack[][32], int count) {
int i;
if (needle == NULL) {
return 0;
}
for (i = 0; i < count; ++i) {
if (strcmp(haystack[i], needle) == 0 || strcmp(haystack[i], "*") == 0) {
return 1;
}
}
return 0;
}
prompt_mode_t prompt_mode_from_str(const char *s) {
if (s == NULL) {
return PROMPT_DENY;
@@ -558,6 +828,12 @@ int parse_preapprove_spec(const char *spec, policy_entry_t *out_entry, int *out_
int nostr_index = -1;
int has_role = 0;
int has_nostr_index = 0;
/* Algorithm-based fields */
int has_algorithm = 0;
int has_index = 0;
int alg_index_min = -1;
int alg_index_max = -1;
int verb_count = 0;
if (out_nostr_index != NULL) {
*out_nostr_index = -1;
@@ -603,6 +879,67 @@ int parse_preapprove_spec(const char *spec, policy_entry_t *out_entry, int *out_
}
nostr_index = (int)val;
has_nostr_index = 1;
} else if (strcmp(token, "algorithm") == 0) {
/* algorithm=<name> — algorithm-based preapprove */
if (verb_count >= 0 && out_entry->alg_count < 16) {
strncpy(out_entry->algorithms[out_entry->alg_count], eq + 1, 31);
out_entry->algorithms[out_entry->alg_count][31] = '\0';
out_entry->alg_count++;
}
has_algorithm = 1;
} else if (strcmp(token, "index") == 0) {
/* index=<N> or index=<N-M> or index=* */
char *val = eq + 1;
if (strcmp(val, "*") == 0) {
alg_index_min = -1;
alg_index_max = -1;
} else {
char *dash = strchr(val, '-');
if (dash != NULL) {
char *endptr = NULL;
long lo, hi;
*dash = '\0';
errno = 0;
lo = strtol(val, &endptr, 10);
if (errno != 0 || endptr == val || *endptr != '\0' || lo < 0) {
fprintf(stderr, "ERROR: --preapprove spec has invalid index= range: %s\n", spec);
free(spec_copy);
return -1;
}
errno = 0;
hi = strtol(dash + 1, &endptr, 10);
if (errno != 0 || endptr == dash + 1 || *endptr != '\0' || hi < lo) {
fprintf(stderr, "ERROR: --preapprove spec has invalid index= range: %s\n", spec);
free(spec_copy);
return -1;
}
alg_index_min = (int)lo;
alg_index_max = (int)hi;
} else {
char *endptr = NULL;
long val_num;
errno = 0;
val_num = strtol(val, &endptr, 10);
if (errno != 0 || endptr == val || *endptr != '\0' || val_num < 0) {
fprintf(stderr, "ERROR: --preapprove spec has invalid index= value: %s\n", spec);
free(spec_copy);
return -1;
}
alg_index_min = (int)val_num;
alg_index_max = (int)val_num;
}
}
} else if (strcmp(token, "verb") == 0) {
/* verb=<name> — can be repeated or comma-separated within the value.
* Since we split on commas already, each verb= is a single verb. */
if (verb_count < POLICY_MAX_VERBS) {
strncpy(out_entry->verbs[verb_count], eq + 1, POLICY_VERB_MAX_LEN - 1);
out_entry->verbs[verb_count][POLICY_VERB_MAX_LEN - 1] = '\0';
verb_count++;
out_entry->verb_count = verb_count;
}
} else {
fprintf(stderr, "ERROR: unknown --preapprove field '%s' in spec: %s\n", token, spec);
free(spec_copy);
@@ -618,38 +955,99 @@ int parse_preapprove_spec(const char *spec, policy_entry_t *out_entry, int *out_
return -1;
}
if (!has_role && !has_nostr_index) {
fprintf(stderr, "ERROR: --preapprove spec must include 'role=' or 'nostr_index=': %s\n", spec);
/* Algorithm-based entries require algorithm= (index= and verb= optional).
* Role-based entries require role= or nostr_index=. The two forms are
* mutually exclusive. */
if (has_algorithm && (has_role || has_nostr_index)) {
fprintf(stderr, "ERROR: --preapprove spec has both algorithm= and role=/nostr_index= (use one form): %s\n", spec);
free(spec_copy);
return -1;
}
if (has_role && has_nostr_index) {
if (!has_algorithm && !has_role && !has_nostr_index) {
fprintf(stderr, "ERROR: --preapprove spec must include 'algorithm=' or 'role=' or 'nostr_index=': %s\n", spec);
free(spec_copy);
return -1;
}
if (!has_algorithm && has_role && has_nostr_index) {
fprintf(stderr, "ERROR: --preapprove spec has both 'role=' and 'nostr_index=' (use one): %s\n", spec);
free(spec_copy);
return -1;
}
memset(out_entry, 0, sizeof(*out_entry));
/* Re-set the algorithm/verb fields since memset cleared them. */
/* (We parsed into out_entry before the memset, so re-parse is needed.
* Actually we parsed algorithm/verb into out_entry directly above.
* The memset wiped them. Let's re-apply by re-parsing... but that's
* complex. Instead, save them before memset and restore after.) */
/* Save algorithm-based fields before memset. */
{
char saved_algs[16][32];
int saved_alg_count = 0;
char saved_verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int saved_verb_count = 0;
int si;
/* We already populated out_entry->algorithms and out_entry->verbs
* during parsing. Save them before the memset below. */
saved_alg_count = out_entry->alg_count;
for (si = 0; si < saved_alg_count; si++) {
memcpy(saved_algs[si], out_entry->algorithms[si], 32);
}
saved_verb_count = out_entry->verb_count;
for (si = 0; si < saved_verb_count; si++) {
memcpy(saved_verbs[si], out_entry->verbs[si], POLICY_VERB_MAX_LEN);
}
memset(out_entry, 0, sizeof(*out_entry));
/* Restore algorithm-based fields. */
out_entry->alg_count = saved_alg_count;
for (si = 0; si < saved_alg_count; si++) {
memcpy(out_entry->algorithms[si], saved_algs[si], 32);
}
out_entry->verb_count = saved_verb_count;
for (si = 0; si < saved_verb_count; si++) {
memcpy(out_entry->verbs[si], saved_verbs[si], POLICY_VERB_MAX_LEN);
}
/* Set index range. */
if (has_algorithm) {
out_entry->index_min = alg_index_min;
out_entry->index_max = alg_index_max;
} else {
out_entry->index_min = -1;
out_entry->index_max = -1;
}
}
strncpy(out_entry->caller, caller, sizeof(out_entry->caller) - 1);
out_entry->caller[sizeof(out_entry->caller) - 1] = '\0';
if (has_role) {
if (has_algorithm) {
/* Algorithm-based entry: no role needed. */
out_entry->role_count = 0;
} else if (has_role) {
strncpy(out_entry->roles[0], role, sizeof(out_entry->roles[0]) - 1);
out_entry->roles[0][sizeof(out_entry->roles[0]) - 1] = '\0';
out_entry->role_count = 1;
} else {
/* nostr_index-based: map to role name. */
if (nostr_index == 0) {
strncpy(out_entry->roles[0], "main", sizeof(out_entry->roles[0]) - 1);
out_entry->roles[0][sizeof(out_entry->roles[0]) - 1] = '\0';
} else {
(void)snprintf(out_entry->roles[0], sizeof(out_entry->roles[0]), "nostr_idx_%d", nostr_index);
}
out_entry->role_count = 1;
if (out_nostr_index != NULL) {
*out_nostr_index = nostr_index;
}
}
out_entry->role_count = 1;
out_entry->prompt = PROMPT_NEVER;
free(spec_copy);
@@ -774,3 +1172,88 @@ int policy_check(const policy_table_t *table, const char *caller_id,
return POLICY_NO_MATCH;
}
/* ---- Algorithm-based policy check ----
*
* policy_check_algorithm() checks whether a caller is allowed to invoke
* a verb with a given algorithm and index. Only entries with alg_count > 0
* (algorithm-based entries) are considered. Role-based entries are
* ignored here (they're handled by policy_check()).
*
* Matching criteria:
* - caller matches (exact or "*")
* - verb matches (or verb_count == 0 = any verb)
* - algorithm matches (or alg_count == 0 = any algorithm, or "*" entry)
* - index is within [index_min, index_max] (or index_min == -1 = any)
*/
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source) {
int i;
if (out_source != NULL) {
*out_source = POLICY_SOURCE_DEFAULT;
}
if (table == NULL || caller_id == NULL || verb == NULL || algorithm == NULL) {
return POLICY_NO_MATCH;
}
for (i = 0; i < table->count; ++i) {
const policy_entry_t *entry = &table->entries[i];
int caller_ok;
int verb_ok;
int alg_ok;
int index_ok;
/* Only consider algorithm-based entries. */
if (entry->alg_count == 0) {
continue;
}
caller_ok = (strcmp(entry->caller, "*") == 0) || (strcmp(entry->caller, caller_id) == 0);
if (!caller_ok) {
continue;
}
verb_ok = (entry->verb_count == 0) ||
string_in_verbs(verb, (char (*)[POLICY_VERB_MAX_LEN])entry->verbs, entry->verb_count);
if (!verb_ok) {
continue;
}
alg_ok = string_in_algorithms(algorithm,
(char (*)[32])entry->algorithms, entry->alg_count);
if (!alg_ok) {
continue;
}
/* Index check: -1 means any index is allowed. */
if (entry->index_min < 0 || entry->index_max < 0) {
index_ok = 1;
} else {
index_ok = (index >= entry->index_min && index <= entry->index_max);
}
if (!index_ok) {
continue;
}
if (out_source != NULL) {
*out_source = entry->source;
}
if (entry->prompt == PROMPT_DENY) {
return POLICY_DENY;
}
if (entry->prompt == PROMPT_NEVER) {
return POLICY_ALLOW;
}
if (out_source != NULL) {
*out_source = POLICY_SOURCE_DEFAULT;
}
return POLICY_PROMPT;
}
return POLICY_NO_MATCH;
}

1768
src/pq_crypto.c Normal file

File diff suppressed because it is too large Load Diff

134
src/pq_drbg.c Normal file
View File

@@ -0,0 +1,134 @@
/* pq_drbg.c — Deterministic PRNG for post-quantum key generation.
*
* Implements a SHAKE-256-based deterministic PRNG that replaces PQClean's
* randombytes() function. This allows deterministic PQ key generation from
* a mnemonic-derived seed: same seed → same randombytes output sequence.
*
* The PRNG is simple: 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. This is not a NIST SP 800-90A
* compliant DRBG, but it is deterministic and sufficient for PQ keygen
* (which only needs the output to be uniformly distributed, which SHAKE
* provides).
*
* Security argument: SHAKE-256 is a XOF (extendable output function) based
* on Keccak. Given a 256-bit seed, the output is computationally
* indistinguishable from random. The counter prevents reuse of output
* blocks. This is the same approach used by PQM4 for deterministic test
* vectors.
*/
#include <string.h>
#include <openssl/evp.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, but we use 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) {
EVP_MD_CTX *ctx;
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
size_t outlen = sizeof(g_buffer);
/* Build input: seed || counter (little-endian) */
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);
ctx = EVP_MD_CTX_new();
if (ctx == NULL) {
return;
}
if (EVP_DigestInit_ex(ctx, EVP_shake256(), NULL) != 1) {
EVP_MD_CTX_free(ctx);
return;
}
if (EVP_DigestUpdate(ctx, seed_block, (size_t)g_seed_len + 8) != 1) {
EVP_MD_CTX_free(ctx);
return;
}
if (EVP_DigestFinalXOF(ctx, g_buffer, outlen) != 1) {
EVP_MD_CTX_free(ctx);
return;
}
EVP_MD_CTX_free(ctx);
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;
}
/* Copy seed (up to 32 bytes; if shorter, zero-pad) */
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;
}
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;
}
void pq_drbg_zeroize(void) {
memset(g_seed, 0, sizeof(g_seed));
memset(g_buffer, 0, sizeof(g_buffer));
g_seed_len = 0;
g_counter = 0;
g_buffer_pos = sizeof(g_buffer);
g_initialized = 0;
}
/* --- randombytes() — the function PQClean calls --- */
int randombytes(unsigned char *buf, size_t len) {
return pq_drbg_randombytes(buf, len);
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -198,6 +203,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -233,6 +256,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -257,6 +282,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -290,6 +321,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -297,15 +342,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -336,6 +513,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -344,10 +597,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -591,6 +845,12 @@ role_purpose_t role_purpose_from_str(const char *s) {
if (str_eq(s, "fips")) {
return PURPOSE_FIPS;
}
if (str_eq(s, "pq-sig")) {
return PURPOSE_PQ_SIG;
}
if (str_eq(s, "pq-kem")) {
return PURPOSE_PQ_KEM;
}
return PURPOSE_UNKNOWN;
}
@@ -604,6 +864,15 @@ role_curve_t role_curve_from_str(const char *s) {
if (str_eq(s, "x25519")) {
return CURVE_X25519;
}
if (str_eq(s, "ml-dsa-65")) {
return CURVE_ML_DSA_65;
}
if (str_eq(s, "slh-dsa-128s")) {
return CURVE_SLH_DSA_128S;
}
if (str_eq(s, "ml-kem-768")) {
return CURVE_ML_KEM_768;
}
return CURVE_UNKNOWN;
}
@@ -619,6 +888,10 @@ const char *role_purpose_to_str(role_purpose_t p) {
return "age";
case PURPOSE_FIPS:
return "fips";
case PURPOSE_PQ_SIG:
return "pq-sig";
case PURPOSE_PQ_KEM:
return "pq-kem";
case PURPOSE_UNKNOWN:
default:
return "unknown";
@@ -633,6 +906,12 @@ const char *role_curve_to_str(role_curve_t c) {
return "ed25519";
case CURVE_X25519:
return "x25519";
case CURVE_ML_DSA_65:
return "ml-dsa-65";
case CURVE_SLH_DSA_128S:
return "slh-dsa-128s";
case CURVE_ML_KEM_768:
return "ml-kem-768";
case CURVE_UNKNOWN:
default:
return "unknown";

View File

@@ -84,6 +84,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -92,6 +94,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -197,6 +202,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -232,6 +255,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -256,6 +281,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -289,6 +320,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -296,15 +341,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -335,6 +512,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -343,10 +596,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -84,6 +84,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -92,6 +94,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -200,6 +205,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -235,6 +258,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -259,6 +284,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -297,6 +328,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -304,15 +349,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -346,6 +523,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -354,10 +607,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -415,6 +669,9 @@ typedef struct {
} caller_identity_t;
/* Server context */
#define INDEX_WHITELIST_MAX 256 /* nostr_index range 0-255 */
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) /* 32 bytes */
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
char last_error[256];
@@ -428,6 +685,8 @@ typedef struct {
int auth_mode;
int auth_skew_seconds;
int bridge_source_trusted; /* when set, unix connections send a qrexec_source preamble */
int index_whitelist_active; /* 1 if index whitelist is set (not "all") */
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; /* bitmap of allowed nostr_index values */
} server_ctx_t;
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
@@ -444,6 +703,15 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
* identity is composed as qubes:<vm>. */
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
/* Set the nostr_index whitelist from a parsed spec string.
* Spec format: "all" (no restriction), or comma-separated list of
* indices and ranges, e.g. "1,3,4" or "0-3" or "0-3,7,9".
* Returns 0 on success, -1 on parse error. */
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
/* Check if a nostr_index is in the whitelist. Returns 1 if allowed, 0 if not. */
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index);
/* Start listening. Returns 0 on success, -1 on error. */
int server_start(server_ctx_t *ctx);
@@ -514,6 +782,7 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -1058,6 +1327,8 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
ctx->auth_mode = auth_mode;
ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
ctx->bridge_source_trusted = 0;
ctx->index_whitelist_active = 0;
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
if (!g_auth_nonce_cache_inited) {
auth_nonce_cache_init(&g_auth_nonce_cache);
g_auth_nonce_cache_inited = 1;
@@ -1071,6 +1342,87 @@ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled) {
ctx->bridge_source_trusted = enabled ? 1 : 0;
}
static void whitelist_set_bit(unsigned char *bitmap, int index) {
if (index >= 0 && index < INDEX_WHITELIST_MAX) {
bitmap[index / 8] |= (1 << (index % 8));
}
}
static int whitelist_get_bit(const unsigned char *bitmap, int index) {
if (index < 0 || index >= INDEX_WHITELIST_MAX) {
return 0;
}
return (bitmap[index / 8] >> (index % 8)) & 1;
}
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec) {
char buf[256];
char *p;
char *token;
if (ctx == NULL || spec == NULL) {
return -1;
}
/* "all" means no restriction */
if (strcmp(spec, "all") == 0) {
ctx->index_whitelist_active = 0;
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
return 0;
}
strncpy(buf, spec, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
/* Parse comma-separated entries: "1,3,4" or "0-3" or "0-3,7,9" */
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
p = buf;
while (p != NULL && *p != '\0') {
char *comma = strchr(p, ',');
if (comma != NULL) {
*comma = '\0';
}
/* Parse token: either "N" or "N-M" (range) */
char *dash = strchr(p, '-');
if (dash != NULL) {
*dash = '\0';
char *endptr1 = NULL, *endptr2 = NULL;
long lo = strtol(p, &endptr1, 10);
long hi = strtol(dash + 1, &endptr2, 10);
if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 ||
lo >= INDEX_WHITELIST_MAX || hi >= INDEX_WHITELIST_MAX || lo > hi) {
return -1;
}
for (long i = lo; i <= hi; i++) {
whitelist_set_bit(ctx->index_whitelist, (int)i);
}
} else {
char *endptr = NULL;
long idx = strtol(p, &endptr, 10);
if (*endptr != '\0' || idx < 0 || idx >= INDEX_WHITELIST_MAX) {
return -1;
}
whitelist_set_bit(ctx->index_whitelist, (int)idx);
}
p = (comma != NULL) ? comma + 1 : NULL;
}
ctx->index_whitelist_active = 1;
return 0;
}
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index) {
if (ctx == NULL) {
return 1; /* no ctx = allow (shouldn't happen) */
}
if (!ctx->index_whitelist_active) {
return 1; /* "all" mode = no restriction */
}
return whitelist_get_bit(ctx->index_whitelist, nostr_index);
}
int server_start(server_ctx_t *ctx) {
int fd;
struct sockaddr_un addr;
@@ -1353,6 +1705,33 @@ int server_get_caller(int fd, caller_identity_t *out) {
return 0;
}
/* ---- Async mine_event support ---- */
typedef struct {
int client_fd;
int is_stdio;
dispatcher_ctx_t *dispatcher;
char *request;
} mine_thread_arg_t;
static void *mine_event_thread(void *arg) {
mine_thread_arg_t *a = (mine_thread_arg_t *)arg;
char *response = NULL;
response = dispatcher_handle_request(a->dispatcher, a->request);
if (response != NULL) {
(void)transport_send_framed(a->is_stdio ? STDOUT_FILENO : a->client_fd, response);
free(response);
}
free(a->request);
if (!a->is_stdio) {
close(a->client_fd);
}
free(a);
return NULL;
}
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
int client_fd;
caller_identity_t caller;
@@ -1569,6 +1948,24 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
}
}
/*
* Index whitelist enforcement: if the request specifies a nostr_index
* and the whitelist is active (not "all"), check that the index is
* allowed. This is a hard deny — it happens before policy/approval,
* so even an approved caller cannot access a non-whitelisted index.
* We set hard_selector_error to a sentinel so the policy_check branch
* is skipped.
*/
if (hard_selector_error == 0 && selector_req.has_nostr_index &&
!server_index_whitelist_allows(ctx, selector_req.nostr_index)) {
(void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2002,\"message\":\"index_not_allowed\"}}");
pchk = POLICY_DENY;
verdict = "DENIED";
source_label = "index-not-approved";
hard_selector_error = -100; /* sentinel: skip policy_check */
}
if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
pchk = POLICY_DENY;
@@ -1578,9 +1975,11 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
} else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
pchk = POLICY_DENY;
} else {
} else if (hard_selector_error == 0) {
/* Normal path: run policy_check (skip if whitelist already denied) */
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src);
}
/* else: hard_selector_error == -100 (whitelist deny) — keep pchk=POLICY_DENY */
if (pchk == POLICY_PROMPT) {
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
@@ -1636,6 +2035,47 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
}
if (pchk == POLICY_ALLOW && !derivation_error) {
/*
* mine_event is a long-running operation (potentially seconds to minutes).
* Spawn a detached thread so the server stays responsive. The thread
* owns the client_fd and request, and sends the response when mining
* completes. We skip the synchronous response path below.
*/
if (strcmp(method, "mine_event") == 0 && client_fd != STDIN_FILENO) {
mine_thread_arg_t *marg = malloc(sizeof(mine_thread_arg_t));
if (marg != NULL) {
pthread_t mine_tid;
marg->client_fd = client_fd;
marg->is_stdio = 0;
marg->dispatcher = ctx->dispatcher;
marg->request = request;
if (pthread_create(&mine_tid, NULL, mine_event_thread, marg) == 0) {
pthread_detach(mine_tid);
/* Thread owns client_fd and request — prevent cleanup below */
request = NULL;
response = NULL;
verdict = "ALLOWED";
source_label = "async-mine";
(void)snprintf(activity,
sizeof(activity),
"%s %s(%s) %s:%s",
caller.caller_id,
method,
role_name,
"ALLOWED",
"async-mine");
if (cb != NULL) {
cb(activity, cb_data);
}
return 1;
}
/* pthread_create failed — fall through to synchronous path */
free(marg);
}
}
verdict = "ALLOWED";
response = dispatcher_handle_request(ctx->dispatcher, request);
if (response == NULL) {
@@ -1661,6 +2101,9 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
source_label = "session-grant";
} else if (pchk == POLICY_ALLOW) {
source_label = "prompt";
} else if (hard_selector_error == -100) {
/* Index whitelist denial — keep the label set by the whitelist check */
/* source_label already set to "index-not-approved" above */
} else {
source_label = "no-match";
}

View File

@@ -84,6 +84,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -92,6 +94,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -197,6 +202,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -232,6 +255,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -256,6 +281,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -289,6 +320,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -296,15 +341,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -335,6 +512,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -343,10 +596,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

693
tests/test_algorithm_api.c Normal file
View File

@@ -0,0 +1,693 @@
/* test_algorithm_api.c — Tests for the algorithm-based API.
*
* Tests the new verbs: sign, verify, encapsulate, decapsulate,
* derive_shared_secret, and the algorithm-based get_public_key.
* Also tests old verb aliases and enforcement.
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum { PURPOSE_NOSTR=0, PURPOSE_BITCOIN, PURPOSE_SSH, PURPOSE_AGE, PURPOSE_FIPS, PURPOSE_PQ_SIG, PURPOSE_PQ_KEM, PURPOSE_UNKNOWN } role_purpose_t;
typedef enum { CURVE_SECP256K1=0, CURVE_ED25519, CURVE_X25519, CURVE_ML_DSA_65, CURVE_SLH_DSA_128S, CURVE_ML_KEM_768, CURVE_UNKNOWN } role_curve_t;
typedef enum { SELECTOR_NOSTR_INDEX, SELECTOR_ROLE_PATH } role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct { role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; int count; } role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
typedef enum { PROMPT_NEVER=0, PROMPT_FIRST_PER_BOOT, PROMPT_EVERY_REQUEST, PROMPT_DENY } prompt_mode_t;
typedef enum { POLICY_SOURCE_DEFAULT=0, POLICY_SOURCE_PREAPPROVE, POLICY_SOURCE_SESSION_GRANT } policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
char algorithms[16][32];
int alg_count;
int index_min;
int index_max;
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct { policy_entry_t entries[POLICY_MAX_ENTRIES]; int count; } policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* from pq_crypto.h */
typedef enum { CRYPTO_ALG_SECP256K1=0, CRYPTO_ALG_ED25519, CRYPTO_ALG_X25519, CRYPTO_ALG_ML_DSA_65, CRYPTO_ALG_SLH_DSA_128S, CRYPTO_ALG_ML_KEM_768, CRYPTO_ALG_UNKNOWN } crypto_alg_t;
typedef struct { size_t priv_key_len; size_t pub_key_len; size_t sig_len; size_t ciphertext_len; size_t shared_secret_len; } crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len, const unsigned char *peer_pub, size_t pub_len, unsigned char *shared_out, size_t *shared_out_len);
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path, unsigned char *seed_out, size_t seed_out_len);
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len, unsigned char *ct_out, unsigned char *ss_out);
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len, const unsigned char *ct, size_t ct_len, unsigned char *ss_out);
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from alg_api.h */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct { alg_key_entry_t entries[ALG_KEY_CACHE_MAX]; int count; } algorithm_key_cache_t;
void alg_key_cache_init(algorithm_key_cache_t *cache);
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from crypto.h */
typedef struct {
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char npub[128];
crypto_alg_t alg;
int valid;
} derived_key_t;
typedef struct { derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; int count; } key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from main.h */
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <nostr_core/utils.h>
#include <nostr_core/nostr_common.h>
static int g_passes = 0;
static int g_total = 0;
static void check(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int resp_has(const char *resp, const char *needle) {
return (resp != NULL && needle != NULL && strstr(resp, needle) != NULL);
}
/* Extract a string field from a JSON result object embedded in a response.
* The response format is {"id":"...","result":"{...}"} where the result
* value is a JSON string. This extracts the result string, parses it,
* and returns the value of the given field. Caller must free. */
static char *extract_result_field(const char *response, const char *field) {
cJSON *root, *result_item, *result_obj, *field_item;
char *out = NULL;
if (response == NULL || field == NULL) return NULL;
root = cJSON_Parse(response);
if (root == NULL) return NULL;
result_item = cJSON_GetObjectItemCaseSensitive(root, "result");
if (!cJSON_IsString(result_item) || result_item->valuestring == NULL) {
cJSON_Delete(root);
return NULL;
}
result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj == NULL) {
cJSON_Delete(root);
return NULL;
}
field_item = cJSON_GetObjectItemCaseSensitive(result_obj, field);
if (cJSON_IsString(field_item) && field_item->valuestring != NULL) {
out = strdup(field_item->valuestring);
}
cJSON_Delete(result_obj);
cJSON_Delete(root);
return out;
}
static int extract_result_bool(const char *response, const char *field) {
cJSON *root, *result_item, *result_obj, *field_item;
int val = -1;
if (response == NULL || field == NULL) return -1;
root = cJSON_Parse(response);
if (root == NULL) return -1;
result_item = cJSON_GetObjectItemCaseSensitive(root, "result");
if (!cJSON_IsString(result_item) || result_item->valuestring == NULL) {
cJSON_Delete(root);
return -1;
}
result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj == NULL) {
cJSON_Delete(root);
return -1;
}
field_item = cJSON_GetObjectItemCaseSensitive(result_obj, field);
if (cJSON_IsBool(field_item)) {
val = cJSON_IsTrue(field_item) ? 1 : 0;
}
cJSON_Delete(result_obj);
cJSON_Delete(root);
return val;
}
/* File-level statics to avoid stack overflow (key_store_t is ~2.1MB) */
static key_store_t g_key_store;
static algorithm_key_cache_t g_alg_key_cache;
static mnemonic_state_t g_mnemonic;
static role_table_t g_role_table;
static dispatcher_ctx_t g_dispatcher;
int main(void) {
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
char *resp;
char *sig_hex;
char *pub_hex;
char *ct_hex;
char *ss_hex1;
char *ss_hex2;
char *shared_hex;
char request[32768];
int rc;
/* ---- Setup ---- */
role_table_init(&g_role_table);
{
role_entry_t main_role;
memset(&main_role, 0, sizeof(main_role));
strncpy(main_role.name, "main", sizeof(main_role.name) - 1);
strncpy(main_role.purpose_str, "nostr", sizeof(main_role.purpose_str) - 1);
strncpy(main_role.curve_str, "secp256k1", sizeof(main_role.curve_str) - 1);
main_role.purpose = role_purpose_from_str("nostr");
main_role.curve = role_curve_from_str("secp256k1");
main_role.selector_type = SELECTOR_NOSTR_INDEX;
main_role.nostr_index = 0;
role_table_add(&g_role_table, &main_role);
}
mnemonic_init(&g_mnemonic);
rc = mnemonic_load(&g_mnemonic, valid_12);
check("mnemonic_load succeeds", rc == 0);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
rc = nostr_init();
check("nostr_init succeeds", rc == 0);
crypto_derive_all(&g_key_store, &g_role_table, &g_mnemonic);
dispatcher_init(&g_dispatcher, &g_role_table, &g_mnemonic, &g_key_store, &g_alg_key_cache);
/* ---- Enforcement tests ---- */
check("enforce sign + ed25519 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ED25519) == ENFORCE_OK);
check("enforce sign + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check("enforce sign + ml-dsa-65 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_DSA_65) == ENFORCE_OK);
check("enforce sign + slh-dsa-128s -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SLH_DSA_128S) == ENFORCE_OK);
check("enforce sign + x25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_X25519) == ENFORCE_ERR_ALGORITHM);
check("enforce sign + ml-kem-768 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM);
check("enforce encapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check("enforce encapsulate + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
check("enforce decapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check("enforce derive_shared_secret + x25519 -> OK",
enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_X25519) == ENFORCE_OK);
check("enforce derive_shared_secret + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
check("enforce get_public_key + any alg -> OK",
enforce_verb_algorithm(VERB_GET_PUBLIC_KEY, CRYPTO_ALG_ED25519) == ENFORCE_OK);
check("enforce sign_event + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check("enforce sign_event + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
/* ---- get_public_key with algorithm parameter ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk1\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("get_public_key algorithm=ed25519 returns result",
resp_has(resp, "\"result\""));
check("get_public_key algorithm=ed25519 has algorithm field",
resp_has(resp, "ed25519"));
check("get_public_key algorithm=ed25519 has public_key field",
resp_has(resp, "public_key"));
check("get_public_key algorithm=ed25519 has key_id field",
resp_has(resp, "key_id"));
free(resp);
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk2\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}");
check("get_public_key algorithm=ml-dsa-65 returns result",
resp_has(resp, "\"result\""));
check("get_public_key algorithm=ml-dsa-65 has algorithm field",
resp_has(resp, "ml-dsa-65"));
free(resp);
/* ---- sign + verify roundtrip: ed25519 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("sign ed25519 returns result",
resp_has(resp, "\"result\""));
check("sign ed25519 has signature field",
resp_has(resp, "signature"));
check("sign ed25519 has algorithm ed25519",
resp_has(resp, "ed25519"));
check("sign ed25519 has key_id",
resp_has(resp, "key_id"));
sig_hex = extract_result_field(resp, "signature");
free(resp);
check("sign ed25519 extracted signature", sig_hex != NULL && strlen(sig_hex) > 0);
if (sig_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"68656c6c6f\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify ed25519 returns result", resp_has(resp, "\"result\""));
check("verify ed25519 valid:true", extract_result_bool(resp, "valid") == 1);
free(resp);
/* verify with wrong message */
snprintf(request, sizeof(request),
"{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"776f726c64\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify ed25519 wrong msg valid:false", extract_result_bool(resp, "valid") == 0);
free(resp);
free(sig_hex);
}
/* ---- sign + verify roundtrip: ml-dsa-65 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"s2\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}");
check("sign ml-dsa-65 returns result", resp_has(resp, "\"result\""));
check("sign ml-dsa-65 has signature", resp_has(resp, "signature"));
sig_hex = extract_result_field(resp, "signature");
free(resp);
check("sign ml-dsa-65 extracted signature", sig_hex != NULL && strlen(sig_hex) > 0);
if (sig_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"v3\",\"method\":\"verify\",\"params\":[\"68656c6c6f\",\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify ml-dsa-65 valid:true", extract_result_bool(resp, "valid") == 1);
free(resp);
free(sig_hex);
}
/* ---- sign + verify roundtrip: slh-dsa-128s ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"s3\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}");
check("sign slh-dsa-128s returns result", resp_has(resp, "\"result\""));
sig_hex = extract_result_field(resp, "signature");
free(resp);
check("sign slh-dsa-128s extracted signature", sig_hex != NULL && strlen(sig_hex) > 0);
if (sig_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"v4\",\"method\":\"verify\",\"params\":[\"68656c6c6f\",\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify slh-dsa-128s valid:true", extract_result_bool(resp, "valid") == 1);
free(resp);
free(sig_hex);
}
/* ---- sign + verify roundtrip: secp256k1 schnorr ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"s4\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"secp256k1\",\"index\":0,\"scheme\":\"schnorr\"}]}");
check("sign secp256k1 schnorr returns result", resp_has(resp, "\"result\""));
sig_hex = extract_result_field(resp, "signature");
free(resp);
check("sign secp256k1 schnorr extracted signature", sig_hex != NULL && strlen(sig_hex) == 128);
if (sig_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"v5\",\"method\":\"verify\",\"params\":[\"68656c6c6f\",\"%s\",{\"algorithm\":\"secp256k1\",\"index\":0,\"scheme\":\"schnorr\"}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify secp256k1 schnorr valid:true", extract_result_bool(resp, "valid") == 1);
free(resp);
free(sig_hex);
}
/* ---- sign + verify roundtrip: secp256k1 ecdsa ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"s5\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"secp256k1\",\"index\":0,\"scheme\":\"ecdsa\"}]}");
check("sign secp256k1 ecdsa returns result", resp_has(resp, "\"result\""));
sig_hex = extract_result_field(resp, "signature");
free(resp);
check("sign secp256k1 ecdsa extracted signature", sig_hex != NULL && strlen(sig_hex) == 128);
if (sig_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"v6\",\"method\":\"verify\",\"params\":[\"68656c6c6f\",\"%s\",{\"algorithm\":\"secp256k1\",\"index\":0,\"scheme\":\"ecdsa\"}]}",
sig_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("verify secp256k1 ecdsa valid:true", extract_result_bool(resp, "valid") == 1);
free(resp);
free(sig_hex);
}
/* ---- encapsulate + decapsulate: ml-kem-768 ---- */
/* First get our ML-KEM public key */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk3\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":0}]}");
pub_hex = extract_result_field(resp, "public_key");
check("get_public_key ml-kem-768 returns public_key", pub_hex != NULL && strlen(pub_hex) > 0);
free(resp);
if (pub_hex) {
/* Encapsulate with our own public key (for testing) */
snprintf(request, sizeof(request),
"{\"id\":\"e1\",\"method\":\"encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}",
pub_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("encapsulate ml-kem-768 returns result", resp_has(resp, "\"result\""));
check("encapsulate ml-kem-768 has ciphertext", resp_has(resp, "ciphertext"));
check("encapsulate ml-kem-768 has shared_secret", resp_has(resp, "shared_secret"));
ct_hex = extract_result_field(resp, "ciphertext");
ss_hex1 = extract_result_field(resp, "shared_secret");
free(resp);
check("encapsulate extracted ciphertext", ct_hex != NULL && strlen(ct_hex) > 0);
check("encapsulate extracted shared_secret", ss_hex1 != NULL && strlen(ss_hex1) > 0);
if (ct_hex) {
/* Decapsulate with our private key */
snprintf(request, sizeof(request),
"{\"id\":\"d1\",\"method\":\"decapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}",
ct_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("decapsulate ml-kem-768 returns result", resp_has(resp, "\"result\""));
check("decapsulate ml-kem-768 has shared_secret", resp_has(resp, "shared_secret"));
ss_hex2 = extract_result_field(resp, "shared_secret");
free(resp);
check("decapsulate extracted shared_secret", ss_hex2 != NULL);
/* Verify the shared secrets match */
if (ss_hex1 && ss_hex2) {
check("encapsulate/decapsulate shared secrets match",
strcmp(ss_hex1, ss_hex2) == 0);
}
free(ss_hex2);
}
free(ct_hex);
free(ss_hex1);
free(pub_hex);
}
/* ---- derive_shared_secret: x25519 ---- */
/* Get our x25519 public key */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk4\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"x25519\",\"index\":0}]}");
pub_hex = extract_result_field(resp, "public_key");
check("get_public_key x25519 returns public_key", pub_hex != NULL && strlen(pub_hex) > 0);
free(resp);
if (pub_hex) {
/* Derive shared secret with our own public key (for testing) */
snprintf(request, sizeof(request),
"{\"id\":\"dss1\",\"method\":\"derive_shared_secret\",\"params\":[\"%s\",{\"algorithm\":\"x25519\",\"index\":0}]}",
pub_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("derive_shared_secret x25519 returns result", resp_has(resp, "\"result\""));
check("derive_shared_secret x25519 has shared_secret", resp_has(resp, "shared_secret"));
check("derive_shared_secret x25519 has algorithm x25519", resp_has(resp, "x25519"));
shared_hex = extract_result_field(resp, "shared_secret");
free(resp);
check("derive_shared_secret extracted shared_secret", shared_hex != NULL && strlen(shared_hex) == 64);
free(shared_hex);
free(pub_hex);
}
/* ---- Enforcement via dispatcher: sign with x25519 fails ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"err1\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"x25519\",\"index\":0}]}");
check("sign x25519 returns algorithm error",
resp_has(resp, "algorithm_not_supported_for_verb") || resp_has(resp, "\"code\":1010"));
free(resp);
/* ---- Enforcement via dispatcher: encapsulate with ed25519 fails ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"err2\",\"method\":\"encapsulate\",\"params\":[\"abcd\",{\"algorithm\":\"ed25519\"}]}");
check("encapsulate ed25519 returns algorithm error",
resp_has(resp, "algorithm_not_supported_for_verb") || resp_has(resp, "\"code\":1010"));
free(resp);
/* ---- Old verb alias: sign_data with algorithm=ed25519 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"alias1\",\"method\":\"sign_data\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("sign_data alias with algorithm=ed25519 returns result",
resp_has(resp, "\"result\""));
check("sign_data alias has signature",
resp_has(resp, "signature"));
free(resp);
/* ---- Old verb alias: ssh_sign with algorithm=ed25519 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"alias2\",\"method\":\"ssh_sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("ssh_sign alias with algorithm=ed25519 returns result",
resp_has(resp, "\"result\""));
free(resp);
/* ---- Old verb alias: kem_encapsulate with algorithm=ml-kem-768 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk5\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":1}]}");
pub_hex = extract_result_field(resp, "public_key");
free(resp);
if (pub_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"alias3\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}",
pub_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("kem_encapsulate alias with algorithm=ml-kem-768 returns result",
resp_has(resp, "\"result\""));
check("kem_encapsulate alias has ciphertext",
resp_has(resp, "ciphertext"));
free(resp);
free(pub_hex);
}
/* ---- Backward compat: sign_event with role still works ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"bc1\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}");
check("sign_event with role=main (backward compat) returns signed event",
resp_has(resp, "pubkey") && resp_has(resp, "sig"));
free(resp);
/* ---- Backward compat: get_public_key with role still works ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"bc2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}");
check("get_public_key with role=main (backward compat) returns hex",
resp_has(resp, "\"result\""));
free(resp);
/* ---- Policy: algorithm-based preapprove ---- */
{
policy_table_t policy;
policy_entry_t entry;
policy_source_t src;
policy_init_default(&policy, 1000);
/* Add algorithm-based preapprove entry */
memset(&entry, 0, sizeof(entry));
strncpy(entry.caller, "uid:1000", sizeof(entry.caller) - 1);
strncpy(entry.algorithms[0], "ed25519", sizeof(entry.algorithms[0]) - 1);
entry.alg_count = 1;
entry.index_min = 0;
entry.index_max = 4;
strncpy(entry.verbs[0], "sign", sizeof(entry.verbs[0]) - 1);
strncpy(entry.verbs[1], "verify", sizeof(entry.verbs[1]) - 1);
entry.verb_count = 2;
entry.prompt = PROMPT_NEVER;
entry.source = POLICY_SOURCE_PREAPPROVE;
policy_table_add(&policy, &entry);
/* Check: allowed */
rc = policy_check_algorithm(&policy, "uid:1000", "sign", "ed25519", 0, &src);
check("policy_check_algorithm: sign ed25519 idx=0 -> ALLOW", rc == POLICY_ALLOW);
rc = policy_check_algorithm(&policy, "uid:1000", "sign", "ed25519", 3, &src);
check("policy_check_algorithm: sign ed25519 idx=3 -> ALLOW", rc == POLICY_ALLOW);
/* Check: index out of range */
rc = policy_check_algorithm(&policy, "uid:1000", "sign", "ed25519", 5, &src);
check("policy_check_algorithm: sign ed25519 idx=5 -> NO_MATCH", rc == POLICY_NO_MATCH);
/* Check: wrong algorithm */
rc = policy_check_algorithm(&policy, "uid:1000", "sign", "ml-dsa-65", 0, &src);
check("policy_check_algorithm: sign ml-dsa-65 -> NO_MATCH", rc == POLICY_NO_MATCH);
/* Check: wrong verb */
rc = policy_check_algorithm(&policy, "uid:1000", "encapsulate", "ed25519", 0, &src);
check("policy_check_algorithm: encapsulate ed25519 -> NO_MATCH", rc == POLICY_NO_MATCH);
/* Check: wrong caller */
rc = policy_check_algorithm(&policy, "uid:2000", "sign", "ed25519", 0, &src);
check("policy_check_algorithm: wrong caller -> NO_MATCH", rc == POLICY_NO_MATCH);
}
/* ---- Cleanup ---- */
crypto_wipe(&g_key_store);
alg_key_cache_wipe(&g_alg_key_cache);
nostr_cleanup();
mnemonic_unload(&g_mnemonic);
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -198,6 +203,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -233,6 +256,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -257,6 +282,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -290,6 +321,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -297,15 +342,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -339,6 +516,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -347,10 +600,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
@@ -517,6 +771,7 @@ int main(void) {
mnemonic_state_t mnemonic;
dispatcher_ctx_t dispatcher;
key_store_t key_store;
static algorithm_key_cache_t alg_key_cache;
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
char *resp;
int derived;
@@ -536,7 +791,8 @@ int main(void) {
mnemonic_load(&mnemonic, valid_12);
memset(&key_store, 0, sizeof(key_store));
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store);
alg_key_cache_init(&alg_key_cache);
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store, &alg_key_cache);
derived = nostr_init();
check_condition("nostr_init succeeds", derived == 0);

901
tests/test_ed25519_x25519.c Normal file
View File

@@ -0,0 +1,901 @@
/* Test for Phase 2 ed25519 + x25519 crypto (pq_crypto.c, key_store.c,
* enforcement.c, dispatcher.c).
*
* Verifies:
* - ed25519 keygen from seed: 32-byte priv/pub
* - ed25519 sign/verify roundtrip + wrong-message fails
* - ed25519 determinism: same seed -> same keypair
* - x25519 keygen from seed: 32-byte priv/pub
* - x25519 ECDH roundtrip: two sides derive matching shared secret
* - x25519 determinism: same seed -> same keypair
* - SLIP-0010 derivation: deterministic seed from mnemonic+path
* - Integration: derive ed25519 key via crypto_derive_one, sign_data via
* dispatcher, verify signature
* - Enforcement: sign_data allowed on ssh+ed25519, rejected on nostr+secp256k1
* - Enforcement: ssh_sign allowed on ssh+ed25519
* - Enforcement: kem_encapsulate/decapsulate rejected on ssh/ed25519
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG,
PURPOSE_PQ_KEM,
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
role_entry_t *role_table_get_default(role_table_t *table);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0,
CRYPTO_ALG_ED25519,
CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65,
CRYPTO_ALG_SLH_DSA_128S,
CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
static algorithm_key_cache_t g_alg_key_cache;
#include <nostr_core/nostr_common.h>
#include <nostr_core/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
static role_entry_t make_ssh_ed25519_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "ssh", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "ed25519", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_nostr_secp_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_pq_kem_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "pq-kem", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "ml-kem-768", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
/* Test mnemonic (BIP-39 standard test vector). */
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
unsigned char seed[32];
unsigned char priv1[32], pub1[32];
unsigned char priv2[32], pub2[32];
unsigned char sig[64];
size_t sig_len;
size_t shared_len;
int rc;
/* ---- ed25519 keygen from seed ---- */
memset(seed, 0x42, 32);
rc = crypto_ed25519_keygen_from_seed(seed, 32, priv1, pub1);
check_condition("ed25519 keygen from seed succeeds", rc == 0);
/* Determinism: same seed -> same keypair */
rc = crypto_ed25519_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ed25519 keygen determinism (same seed -> same key)", rc == 0 &&
memcmp(priv1, priv2, 32) == 0 && memcmp(pub1, pub2, 32) == 0);
/* Different seed -> different keypair */
memset(seed, 0x99, 32);
rc = crypto_ed25519_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ed25519 keygen different seed -> different key", rc == 0 &&
memcmp(priv1, priv2, 32) != 0);
/* ---- ed25519 sign/verify roundtrip ---- */
memset(seed, 0x42, 32);
crypto_ed25519_keygen_from_seed(seed, 32, priv1, pub1);
{
const char *msg = "hello world";
sig_len = sizeof(sig);
rc = crypto_ed25519_sign(priv1, 32, (const unsigned char *)msg, strlen(msg), sig, &sig_len);
check_condition("ed25519 sign succeeds", rc == 0 && sig_len == 64);
rc = crypto_ed25519_verify(pub1, 32, (const unsigned char *)msg, strlen(msg), sig, sig_len);
check_condition("ed25519 verify valid signature", rc == 0);
/* Wrong message should fail */
const char *wrong_msg = "hello worle";
rc = crypto_ed25519_verify(pub1, 32, (const unsigned char *)wrong_msg, strlen(wrong_msg), sig, sig_len);
check_condition("ed25519 verify wrong message -> invalid", rc == 1);
/* Wrong key should fail */
memset(seed, 0xAB, 32);
crypto_ed25519_keygen_from_seed(seed, 32, priv2, pub2);
rc = crypto_ed25519_verify(pub2, 32, (const unsigned char *)msg, strlen(msg), sig, sig_len);
check_condition("ed25519 verify wrong key -> invalid", rc == 1);
}
/* ---- x25519 keygen from seed ---- */
memset(seed, 0x33, 32);
rc = crypto_x25519_keygen_from_seed(seed, 32, priv1, pub1);
check_condition("x25519 keygen from seed succeeds", rc == 0);
memset(seed, 0x33, 32);
rc = crypto_x25519_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("x25519 keygen determinism (same seed -> same key)", rc == 0 &&
memcmp(priv1, priv2, 32) == 0 && memcmp(pub1, pub2, 32) == 0);
/* ---- x25519 ECDH roundtrip ---- */
{
unsigned char alice_priv[32], alice_pub[32];
unsigned char bob_priv[32], bob_pub[32];
unsigned char alice_shared[32], bob_shared[32];
memset(seed, 0xAA, 32);
crypto_x25519_keygen_from_seed(seed, 32, alice_priv, alice_pub);
memset(seed, 0xBB, 32);
crypto_x25519_keygen_from_seed(seed, 32, bob_priv, bob_pub);
shared_len = 32;
rc = crypto_x25519_ecdh(alice_priv, 32, bob_pub, 32, alice_shared, &shared_len);
check_condition("x25519 ECDH alice side succeeds", rc == 0 && shared_len == 32);
shared_len = 32;
rc = crypto_x25519_ecdh(bob_priv, 32, alice_pub, 32, bob_shared, &shared_len);
check_condition("x25519 ECDH bob side succeeds", rc == 0 && shared_len == 32);
check_condition("x25519 ECDH shared secrets match",
memcmp(alice_shared, bob_shared, 32) == 0);
}
/* ---- SLIP-0010 derivation from mnemonic ---- */
{
unsigned char seed_a[32], seed_b[32];
rc = crypto_derive_seed_from_mnemonic(mnemonic, "m/44'/102001'/0'/0'/0'", seed_a, 32);
check_condition("SLIP-0010 derive seed succeeds", rc == 0);
rc = crypto_derive_seed_from_mnemonic(mnemonic, "m/44'/102001'/0'/0'/0'", seed_b, 32);
check_condition("SLIP-0010 derive seed determinism", rc == 0 &&
memcmp(seed_a, seed_b, 32) == 0);
/* Different path -> different seed */
rc = crypto_derive_seed_from_mnemonic(mnemonic, "m/44'/102001'/1'/0'/0'", seed_b, 32);
check_condition("SLIP-0010 different path -> different seed", rc == 0 &&
memcmp(seed_a, seed_b, 32) != 0);
/* Non-hardened path should fail (SLIP-0010 requirement) */
rc = crypto_derive_seed_from_mnemonic(mnemonic, "m/44'/102001'/0'/0/0", seed_b, 32);
check_condition("SLIP-0010 non-hardened path rejected", rc == -1);
}
/* ---- Integration: derive ed25519 key via crypto_derive_one ---- */
{
role_table_t table;
role_entry_t ssh_role;
mnemonic_state_t mnemonic_state;
key_store_t key_store;
int derived;
role_table_init(&table);
ssh_role = make_ssh_ed25519_entry("ssh_main", 0);
role_table_add(&table, &ssh_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&g_alg_key_cache);
alg_key_cache_init(&g_alg_key_cache);
derived = crypto_derive_all(&key_store, &table, &mnemonic_state);
check_condition("crypto_derive_all derives ed25519 key", derived == 1);
{
const char *pub_hex = crypto_get_pubkey_hex(&key_store, 0);
check_condition("ed25519 derived pubkey hex is 64 chars",
pub_hex != NULL && strlen(pub_hex) == 64);
}
{
const unsigned char *priv = crypto_get_private_key(&key_store, 0);
check_condition("ed25519 derived private key not NULL", priv != NULL);
}
crypto_wipe(&key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
{
role_table_t table;
role_entry_t ssh_role;
mnemonic_state_t mnemonic_state;
key_store_t key_store;
dispatcher_ctx_t dispatcher;
char *resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" in hex */
char request[512];
role_table_init(&table);
ssh_role = make_ssh_ed25519_entry("ssh_main", 0);
role_table_add(&table, &ssh_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&g_alg_key_cache);
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache);
/* sign_data request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm ed25519",
resp != NULL && response_has(resp, "ed25519"));
free(resp);
/* ssh_sign request */
snprintf(request, sizeof(request),
"{\"id\":\"test2\",\"method\":\"ssh_sign\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("ssh_sign via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("ssh_sign result contains signature",
resp != NULL && response_has(resp, "signature"));
free(resp);
crypto_wipe(&key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on ssh+ed25519 ---- */
{
role_entry_t ssh_ed = make_ssh_ed25519_entry("ssh", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
role_entry_t pq_kem = make_pq_kem_entry("kem", 0);
check_condition("enforce sign_data + ssh/ed25519 -> OK",
enforce_verb_role(VERB_SIGN_DATA, &ssh_ed) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce ssh_sign + ssh/ed25519 -> OK",
enforce_verb_role(VERB_SSH_SIGN, &ssh_ed) == ENFORCE_OK);
check_condition("enforce ssh_sign + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SSH_SIGN, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign_event + ssh/ed25519 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE);
check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce kem_encapsulate + ssh/ed25519 -> PURPOSE err",
enforce_verb_role(VERB_KEM_ENCAPS, &ssh_ed) == ENFORCE_ERR_PURPOSE);
}
/* ---- Dispatcher: kem_encapsulate with invalid pubkey length ---- */
{
role_table_t table;
role_entry_t kem_role;
mnemonic_state_t mnemonic_state;
key_store_t key_store;
dispatcher_ctx_t dispatcher;
char *resp;
const char *request = "{\"id\":\"k1\",\"method\":\"kem_encapsulate\",\"params\":[\"00\",{\"role\":\"kem_main\"}]}";
role_table_init(&table);
kem_role = make_pq_kem_entry("kem_main", 0);
role_table_add(&table, &kem_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&g_alg_key_cache);
alg_key_cache_init(&g_alg_key_cache);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache);
/* kem_encapsulate is now implemented (Phase 5). With a too-short
* pubkey hex ("00" = 1 byte, but ML-KEM-768 needs 1184 bytes),
* the dispatcher should return an invalid_pubkey_length error. */
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("kem_encapsulate with short pubkey returns error",
resp != NULL && response_has(resp, "\"error\""));
free(resp);
crypto_wipe(&key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Dispatcher: verify_signature roundtrip ---- */
{
role_table_t table;
role_entry_t ssh_role;
mnemonic_state_t mnemonic_state;
key_store_t key_store;
dispatcher_ctx_t dispatcher;
char *sign_resp, *verify_resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" */
char sign_req[512];
char verify_req[1024];
cJSON *sign_json, *result_item, *sig_item;
const char *sig_str = NULL;
role_table_init(&table);
ssh_role = make_ssh_ed25519_entry("ssh_main", 0);
role_table_add(&table, &ssh_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&g_alg_key_cache);
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache);
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
sign_json = cJSON_Parse(sign_resp);
if (sign_json != NULL) {
result_item = cJSON_GetObjectItemCaseSensitive(sign_json, "result");
if (cJSON_IsString(result_item)) {
cJSON *result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj != NULL) {
sig_item = cJSON_GetObjectItemCaseSensitive(result_obj, "signature");
if (cJSON_IsString(sig_item)) {
sig_str = strdup(sig_item->valuestring);
}
cJSON_Delete(result_obj);
}
}
cJSON_Delete(sign_json);
}
}
check_condition("verify roundtrip: extracted signature hex", sig_str != NULL);
/* Verify */
if (sig_str != NULL) {
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"ssh_main\"}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"ssh_main\"}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
}
free(sign_resp);
crypto_wipe(&key_store);
mnemonic_unload(&mnemonic_state);
}
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -84,6 +84,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -92,6 +94,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -197,6 +202,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -232,6 +255,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -256,6 +281,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -289,6 +320,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -296,15 +341,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -335,6 +512,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -343,10 +596,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

703
tests/test_mine_event.c Normal file
View File

@@ -0,0 +1,703 @@
/* Test for mine_event verb (NIP-13 Proof-of-Work) */
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
role_entry_t *role_table_get_default(role_table_t *table);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
char *crypto_mine_event(const key_store_t *store, int role_index,
const char *event_json, int difficulty,
int threads, int timeout_sec);
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/nostr_common.h>
#include <nostr_core/nip013.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
/* Parse a dispatcher response and extract the result object (for mine_event).
* Returns a newly-allocated cJSON result object, or NULL on failure.
* Caller must delete the returned object. */
static cJSON *extract_result_object(const char *response) {
cJSON *resp_json = NULL;
cJSON *result_str = NULL;
cJSON *result_obj = NULL;
if (response == NULL) return NULL;
resp_json = cJSON_Parse(response);
if (!resp_json) return NULL;
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
if (!result_str || !cJSON_IsString(result_str)) {
cJSON_Delete(resp_json);
return NULL;
}
result_obj = cJSON_Parse(result_str->valuestring);
cJSON_Delete(resp_json);
return result_obj;
}
/* Check if target_reached in the result object matches expected */
static int check_target_reached(const char *response, int expected) {
cJSON *result_obj = extract_result_object(response);
cJSON *tr;
int val = -1;
if (!result_obj) return 0;
tr = cJSON_GetObjectItemCaseSensitive(result_obj, "target_reached");
if (tr && cJSON_IsBool(tr)) {
val = cJSON_IsTrue(tr) ? 1 : 0;
}
cJSON_Delete(result_obj);
return (val == expected);
}
static role_entry_t make_nostr_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
role_table_t table;
role_entry_t main_role;
mnemonic_state_t mnemonic;
dispatcher_ctx_t dispatcher;
static key_store_t key_store;
static algorithm_key_cache_t alg_key_cache;
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
char *resp;
int derived;
role_table_init(&table);
main_role = make_nostr_entry("main", 0);
role_table_add(&table, &main_role);
mnemonic_init(&mnemonic);
mnemonic_load(&mnemonic, valid_12);
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&alg_key_cache);
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store, &alg_key_cache);
derived = nostr_init();
check_condition("nostr_init succeeds", derived == 0);
derived = crypto_derive_all(&key_store, &table, &mnemonic);
check_condition("crypto_derive_all derives at least one key", derived >= 1);
/* 1. mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":2,\"threads\":2,\"timeout_sec\":5}]}");
check_condition("mine_event low difficulty returns result object",
response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event low difficulty returns signed event with nonce tag",
response_has(resp, "nonce") && response_has(resp, "sig") && response_has(resp, "pubkey"));
check_condition("mine_event low difficulty target_reached is true",
check_target_reached(resp, 1));
free(resp);
/* 2. mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hard pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":30,\"threads\":4,\"timeout_sec\":2}]}");
check_condition("mine_event high difficulty returns result object",
response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event high difficulty target_reached is false",
check_target_reached(resp, 0));
check_condition("mine_event high difficulty still returns a signed event",
response_has(resp, "nonce") && response_has(resp, "sig"));
free(resp);
/* 3. mine_event with only timeout (no difficulty) — should mine for full duration */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"timeout only\\\",\\\"tags\\\":[]}\","
"{\"threads\":2,\"timeout_sec\":2}]}");
check_condition("mine_event timeout-only returns result",
response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event timeout-only target_reached is false (no target set)",
check_target_reached(resp, 0));
free(resp);
/* 4. mine_event with neither difficulty nor timeout — should error */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"no params\\\",\\\"tags\\\":[]}\","
"{\"threads\":2}]}");
check_condition("mine_event with no termination condition returns 1007",
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1007") && response_has(resp, "no_termination_condition"));
free(resp);
/* 5. mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"difficulty only\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":1,\"threads\":2}]}");
check_condition("mine_event difficulty-only returns result and reaches target",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"result\"") && check_target_reached(resp, 1));
free(resp);
/* 6. mine_event with invalid event JSON */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"6\",\"method\":\"mine_event\",\"params\":["
"\"not valid json\","
"{\"difficulty\":1,\"timeout_sec\":2}]}");
check_condition("mine_event with invalid event returns error",
response_has(resp, "\"id\":\"6\"") && (response_has(resp, "\"code\":1008") || response_has(resp, "\"code\":-32602")));
free(resp);
/* 7. Verify the mined event's difficulty via nip013 validation */
{
cJSON *resp_json, *result_str, *result_obj, *event_str, *event_json, *id_item;
const char *id_hex;
int pow_diff;
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"7\",\"method\":\"mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"verify pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":4,\"threads\":4,\"timeout_sec\":5}]}");
resp_json = cJSON_Parse(resp);
if (resp_json) {
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
if (result_str && cJSON_IsString(result_str)) {
result_obj = cJSON_Parse(result_str->valuestring);
if (result_obj) {
event_str = cJSON_GetObjectItemCaseSensitive(result_obj, "event");
if (event_str && cJSON_IsString(event_str)) {
event_json = cJSON_Parse(event_str->valuestring);
if (event_json) {
id_item = cJSON_GetObjectItemCaseSensitive(event_json, "id");
if (id_item && cJSON_IsString(id_item)) {
id_hex = cJSON_GetStringValue(id_item);
pow_diff = nostr_calculate_pow_difficulty(id_hex);
check_condition("mined event has difficulty >= 4",
pow_diff >= 4);
} else {
check_condition("mined event has difficulty >= 4", 0);
}
cJSON_Delete(event_json);
} else {
check_condition("mined event has difficulty >= 4", 0);
}
} else {
check_condition("mined event has difficulty >= 4", 0);
}
cJSON_Delete(result_obj);
} else {
check_condition("mined event has difficulty >= 4", 0);
}
} else {
check_condition("mined event has difficulty >= 4", 0);
}
cJSON_Delete(resp_json);
} else {
check_condition("mined event has difficulty >= 4", 0);
}
free(resp);
}
crypto_wipe(&key_store);
nostr_cleanup();
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

836
tests/test_ml_dsa_65.c Normal file
View File

@@ -0,0 +1,836 @@
/* Test for Phase 3 ML-DSA-65 (FIPS 204) post-quantum signatures.
*
* Verifies:
* - ML-DSA-65 keygen from seed: 1952-byte pub, 4032-byte priv
* - ML-DSA-65 keygen determinism: same seed -> same keypair
* - ML-DSA-65 sign/verify roundtrip: sign a message, verify passes
* - ML-DSA-65 sign/verify with wrong message: verify fails
* - ML-DSA-65 sign/verify with wrong key: verify fails
* - DRBG determinism: same seed -> same randombytes output
* - Integration: derive ML-DSA-65 key from mnemonic via crypto_derive_one,
* sign data via sign_data verb through dispatcher, verify signature
* - Enforcement: sign_data allowed on pq-sig+ml-dsa-65
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG,
PURPOSE_PQ_KEM,
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0,
CRYPTO_ALG_ED25519,
CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65,
CRYPTO_ALG_SLH_DSA_128S,
CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
static key_store_t g_key_store;
static algorithm_key_cache_t g_alg_key_cache;
#include <nostr_core/nostr_common.h>
#include <nostr_core/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ML-DSA-65 key sizes (FIPS 204) */
#define ML_DSA_65_PUBKEY_BYTES 1952
#define ML_DSA_65_PRIVKEY_BYTES 4032
#define ML_DSA_65_SIG_BYTES 3309
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
static role_entry_t make_pq_sig_ml_dsa_65_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "pq-sig", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "ml-dsa-65", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_nostr_secp_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
/* Test mnemonic (BIP-39 standard test vector). */
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
unsigned char seed[32];
unsigned char *priv1, *pub1, *priv2, *pub2;
unsigned char *sig;
size_t sig_len;
int rc;
/* Allocate key buffers on the heap (they're large) */
priv1 = (unsigned char *)malloc(ML_DSA_65_PRIVKEY_BYTES);
pub1 = (unsigned char *)malloc(ML_DSA_65_PUBKEY_BYTES);
priv2 = (unsigned char *)malloc(ML_DSA_65_PRIVKEY_BYTES);
pub2 = (unsigned char *)malloc(ML_DSA_65_PUBKEY_BYTES);
sig = (unsigned char *)malloc(ML_DSA_65_SIG_BYTES);
if (!priv1 || !pub1 || !priv2 || !pub2 || !sig) {
printf("FAIL: memory allocation\n");
return 1;
}
/* ---- DRBG determinism ---- */
{
unsigned char out1[64], out2[64];
memset(seed, 0x42, 32);
pq_drbg_init(seed, 32);
rc = pq_drbg_randombytes(out1, 64);
check_condition("DRBG randombytes succeeds", rc == 0);
pq_drbg_zeroize();
memset(seed, 0x42, 32);
pq_drbg_init(seed, 32);
rc = pq_drbg_randombytes(out2, 64);
check_condition("DRBG randombytes succeeds (2nd)", rc == 0);
check_condition("DRBG determinism (same seed -> same output)",
memcmp(out1, out2, 64) == 0);
pq_drbg_zeroize();
}
/* ---- ML-DSA-65 keygen from seed ---- */
memset(seed, 0x42, 32);
rc = crypto_ml_dsa_65_keygen_from_seed(seed, 32, priv1, pub1);
check_condition("ML-DSA-65 keygen from seed succeeds", rc == 0);
/* Keygen determinism: same seed -> same keypair */
memset(seed, 0x42, 32);
rc = crypto_ml_dsa_65_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ML-DSA-65 keygen determinism (same seed -> same key)", rc == 0 &&
memcmp(priv1, priv2, ML_DSA_65_PRIVKEY_BYTES) == 0 &&
memcmp(pub1, pub2, ML_DSA_65_PUBKEY_BYTES) == 0);
/* Different seed -> different keypair */
memset(seed, 0x99, 32);
rc = crypto_ml_dsa_65_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ML-DSA-65 keygen different seed -> different key", rc == 0 &&
memcmp(pub1, pub2, ML_DSA_65_PUBKEY_BYTES) != 0);
/* ---- ML-DSA-65 sign/verify roundtrip ---- */
memset(seed, 0x42, 32);
crypto_ml_dsa_65_keygen_from_seed(seed, 32, priv1, pub1);
{
const char *msg = "hello world";
sig_len = ML_DSA_65_SIG_BYTES;
rc = crypto_ml_dsa_65_sign(priv1, ML_DSA_65_PRIVKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, &sig_len);
check_condition("ML-DSA-65 sign succeeds", rc == 0 && sig_len > 0);
rc = crypto_ml_dsa_65_verify(pub1, ML_DSA_65_PUBKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, sig_len);
check_condition("ML-DSA-65 verify valid signature", rc == 0);
/* Wrong message should fail */
{
const char *wrong_msg = "hello worle";
rc = crypto_ml_dsa_65_verify(pub1, ML_DSA_65_PUBKEY_BYTES,
(const unsigned char *)wrong_msg, strlen(wrong_msg),
sig, sig_len);
check_condition("ML-DSA-65 verify wrong message -> invalid", rc == 1);
}
/* Wrong key should fail */
memset(seed, 0xAB, 32);
crypto_ml_dsa_65_keygen_from_seed(seed, 32, priv2, pub2);
rc = crypto_ml_dsa_65_verify(pub2, ML_DSA_65_PUBKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, sig_len);
check_condition("ML-DSA-65 verify wrong key -> invalid", rc == 1);
}
/* ---- Integration: derive ML-DSA-65 key via crypto_derive_one ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
int derived;
role_table_init(&table);
pq_role = make_pq_sig_ml_dsa_65_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
derived = crypto_derive_all(&g_key_store, &table, &mnemonic_state);
check_condition("crypto_derive_all derives ML-DSA-65 key", derived == 1);
{
const char *pub_hex = crypto_get_pubkey_hex(&g_key_store, 0);
/* 1952 bytes hex-encoded = 3904 hex chars + null */
check_condition("ML-DSA-65 derived pubkey hex is 3904 chars",
pub_hex != NULL && strlen(pub_hex) == 3904);
}
{
const unsigned char *priv = crypto_get_private_key(&g_key_store, 0);
check_condition("ML-DSA-65 derived private key not NULL", priv != NULL);
}
/* Determinism: re-derive and check same key */
{
static key_store_t key_store2;
memset(&key_store2, 0, sizeof(key_store2));
crypto_derive_all(&key_store2, &table, &mnemonic_state);
{
const char *pub_hex1 = crypto_get_pubkey_hex(&g_key_store, 0);
const char *pub_hex2 = crypto_get_pubkey_hex(&key_store2, 0);
check_condition("ML-DSA-65 derivation determinism (same mnemonic -> same key)",
pub_hex1 != NULL && pub_hex2 != NULL &&
strcmp(pub_hex1, pub_hex2) == 0);
}
crypto_wipe(&key_store2);
}
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" in hex */
char request[512];
role_table_init(&table);
pq_role = make_pq_sig_ml_dsa_65_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* sign_data request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm ml-dsa-65",
resp != NULL && response_has(resp, "ml-dsa-65"));
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: verify_signature roundtrip via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *sign_resp, *verify_resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" */
char sign_req[512];
char verify_req[8192];
cJSON *sign_json, *result_item, *sig_item;
const char *sig_str = NULL;
role_table_init(&table);
pq_role = make_pq_sig_ml_dsa_65_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
sign_json = cJSON_Parse(sign_resp);
if (sign_json != NULL) {
result_item = cJSON_GetObjectItemCaseSensitive(sign_json, "result");
if (cJSON_IsString(result_item)) {
cJSON *result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj != NULL) {
sig_item = cJSON_GetObjectItemCaseSensitive(result_obj, "signature");
if (cJSON_IsString(sig_item)) {
sig_str = strdup(sig_item->valuestring);
}
cJSON_Delete(result_obj);
}
}
cJSON_Delete(sign_json);
}
}
check_condition("verify roundtrip: extracted signature hex", sig_str != NULL);
/* Verify */
if (sig_str != NULL) {
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
}
free(sign_resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on pq-sig+ml-dsa-65 ---- */
{
role_entry_t pq_sig = make_pq_sig_ml_dsa_65_entry("pq", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce sign_data + pq-sig/ml-dsa-65 -> OK",
enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce verify_signature + pq-sig/ml-dsa-65 -> OK",
enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK);
check_condition("enforce sign_event + pq-sig/ml-dsa-65 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_EVENT, &pq_sig) == ENFORCE_ERR_PURPOSE);
}
/* Cleanup */
free(priv1);
free(pub1);
free(priv2);
free(pub2);
free(sig);
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

668
tests/test_ml_kem_768.c Normal file
View File

@@ -0,0 +1,668 @@
/* Test for Phase 5 ML-KEM-768 (FIPS 203) post-quantum key encapsulation.
*
* Verifies:
* - ML-KEM-768 keygen from seed: 1184-byte pub, 2400-byte priv
* - ML-KEM-768 keygen determinism: same seed -> same keypair
* - Encaps/decaps roundtrip: shared secrets match
* - Encaps produces different ciphertexts each call (non-deterministic)
* - Decaps with wrong ciphertext: different shared secret (implicit rejection)
* - Integration: derive ML-KEM-768 key from mnemonic, encaps via dispatcher,
* decaps via dispatcher, shared secrets match
* - Enforcement: kem_encapsulate/kem_decapsulate allowed on pq-kem+ml-kem-768
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0, PURPOSE_BITCOIN, PURPOSE_SSH, PURPOSE_AGE,
PURPOSE_FIPS, PURPOSE_PQ_SIG, PURPOSE_PQ_KEM, PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0, CURVE_ED25519, CURVE_X25519,
CURVE_ML_DSA_65, CURVE_SLH_DSA_128S, CURVE_ML_KEM_768, CURVE_UNKNOWN
} role_curve_t;
typedef enum { SELECTOR_NOSTR_INDEX, SELECTOR_ROLE_PATH } role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum { PROMPT_NEVER = 0, PROMPT_FIRST_PER_BOOT, PROMPT_EVERY_REQUEST, PROMPT_DENY } prompt_mode_t;
typedef enum { POLICY_SOURCE_DEFAULT = 0, POLICY_SOURCE_PREAPPROVE, POLICY_SOURCE_SESSION_GRANT } policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id, const char *verb, const char *role_name, const char *purpose, policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, CRYPTO_ALG_ED25519, CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65, CRYPTO_ALG_SLH_DSA_128S, CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len, const unsigned char *peer_pub, size_t pub_len, unsigned char *shared_out, size_t *shared_out_len);
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path, unsigned char *seed_out, size_t seed_out_len);
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len);
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic). */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len, unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len, const unsigned char *ct, size_t ct_len, unsigned char *ss_out);
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
typedef struct {
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char npub[128];
crypto_alg_t alg;
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
static key_store_t g_key_store;
static algorithm_key_cache_t g_alg_key_cache;
#include <nostr_core/nostr_common.h>
#include <nostr_core/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ML-KEM-768 key sizes (FIPS 203) */
#define ML_KEM_768_PUBKEY_BYTES 1184
#define ML_KEM_768_PRIVKEY_BYTES 2400
#define ML_KEM_768_CT_BYTES 1088
#define ML_KEM_768_SS_BYTES 32
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
static role_entry_t make_pq_kem_ml_kem_768_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "pq-kem", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "ml-kem-768", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_nostr_secp_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
unsigned char seed[32];
unsigned char *priv1, *pub1, *priv2, *pub2;
unsigned char *ct1, *ct2, *ss1, *ss2, *ss3;
int rc;
priv1 = (unsigned char *)malloc(ML_KEM_768_PRIVKEY_BYTES);
pub1 = (unsigned char *)malloc(ML_KEM_768_PUBKEY_BYTES);
priv2 = (unsigned char *)malloc(ML_KEM_768_PRIVKEY_BYTES);
pub2 = (unsigned char *)malloc(ML_KEM_768_PUBKEY_BYTES);
ct1 = (unsigned char *)malloc(ML_KEM_768_CT_BYTES);
ct2 = (unsigned char *)malloc(ML_KEM_768_CT_BYTES);
ss1 = (unsigned char *)malloc(ML_KEM_768_SS_BYTES);
ss2 = (unsigned char *)malloc(ML_KEM_768_SS_BYTES);
ss3 = (unsigned char *)malloc(ML_KEM_768_SS_BYTES);
if (!priv1 || !pub1 || !priv2 || !pub2 || !ct1 || !ct2 || !ss1 || !ss2 || !ss3) {
printf("FAIL: memory allocation\n");
return 1;
}
/* ---- ML-KEM-768 keygen from seed ---- */
memset(seed, 0x42, 32);
rc = crypto_ml_kem_768_keygen_from_seed(seed, 32, priv1, pub1);
check_condition("ML-KEM-768 keygen from seed succeeds", rc == 0);
/* Keygen determinism: same seed -> same keypair */
memset(seed, 0x42, 32);
rc = crypto_ml_kem_768_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ML-KEM-768 keygen determinism (same seed -> same key)", rc == 0 &&
memcmp(priv1, priv2, ML_KEM_768_PRIVKEY_BYTES) == 0 &&
memcmp(pub1, pub2, ML_KEM_768_PUBKEY_BYTES) == 0);
/* Different seed -> different keypair */
memset(seed, 0x99, 32);
rc = crypto_ml_kem_768_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("ML-KEM-768 keygen different seed -> different key", rc == 0 &&
memcmp(pub1, pub2, ML_KEM_768_PUBKEY_BYTES) != 0);
/* ---- Encaps/decaps roundtrip ---- */
memset(seed, 0x42, 32);
crypto_ml_kem_768_keygen_from_seed(seed, 32, priv1, pub1);
rc = crypto_ml_kem_768_encaps(pub1, ML_KEM_768_PUBKEY_BYTES, ct1, ss1);
check_condition("ML-KEM-768 encaps succeeds", rc == 0);
rc = crypto_ml_kem_768_decaps(priv1, ML_KEM_768_PRIVKEY_BYTES, ct1, ML_KEM_768_CT_BYTES, ss2);
check_condition("ML-KEM-768 decaps succeeds", rc == 0);
check_condition("ML-KEM-768 encaps/decaps shared secrets match",
memcmp(ss1, ss2, ML_KEM_768_SS_BYTES) == 0);
/* ---- Encaps produces different ciphertexts each call ---- */
rc = crypto_ml_kem_768_encaps(pub1, ML_KEM_768_PUBKEY_BYTES, ct2, ss3);
check_condition("ML-KEM-768 second encaps succeeds", rc == 0);
check_condition("ML-KEM-768 encaps produces different ciphertexts",
memcmp(ct1, ct2, ML_KEM_768_CT_BYTES) != 0);
/* Decaps with second ciphertext also matches its shared secret */
{
unsigned char ss4[32];
crypto_ml_kem_768_decaps(priv1, ML_KEM_768_PRIVKEY_BYTES, ct2, ML_KEM_768_CT_BYTES, ss4);
check_condition("ML-KEM-768 second encaps/decaps shared secrets match",
memcmp(ss3, ss4, ML_KEM_768_SS_BYTES) == 0);
}
/* ---- Decaps with wrong ciphertext: implicit rejection ---- */
{
ct1[0] ^= 0xFF;
rc = crypto_ml_kem_768_decaps(priv1, ML_KEM_768_PRIVKEY_BYTES, ct1, ML_KEM_768_CT_BYTES, ss3);
ct1[0] ^= 0xFF; /* restore */
check_condition("ML-KEM-768 decaps wrong ct succeeds (no crash)", rc == 0);
/* The shared secret should be different from the original (implicit rejection) */
check_condition("ML-KEM-768 wrong ct gives different shared secret",
memcmp(ss1, ss3, ML_KEM_768_SS_BYTES) != 0);
}
/* ---- Integration: derive ML-KEM-768 key via crypto_derive_all ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
int derived;
role_table_init(&table);
pq_role = make_pq_kem_ml_kem_768_entry("pq_kem", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
derived = crypto_derive_all(&g_key_store, &table, &mnemonic_state);
check_condition("crypto_derive_all derives ML-KEM-768 key", derived == 1);
{
const char *pub_hex = crypto_get_pubkey_hex(&g_key_store, 0);
/* 1184 bytes hex-encoded = 2368 hex chars + null */
check_condition("ML-KEM-768 derived pubkey hex is 2368 chars",
pub_hex != NULL && strlen(pub_hex) == 2368);
}
{
const unsigned char *priv = crypto_get_private_key(&g_key_store, 0);
check_condition("ML-KEM-768 derived private key not NULL", priv != NULL);
}
/* Determinism: re-derive and check same key */
{
static key_store_t key_store2;
memset(&key_store2, 0, sizeof(key_store2));
crypto_derive_all(&key_store2, &table, &mnemonic_state);
{
const char *pub_hex1 = crypto_get_pubkey_hex(&g_key_store, 0);
const char *pub_hex2 = crypto_get_pubkey_hex(&key_store2, 0);
check_condition("ML-KEM-768 derivation determinism",
pub_hex1 != NULL && pub_hex2 != NULL &&
strcmp(pub_hex1, pub_hex2) == 0);
}
crypto_wipe(&key_store2);
}
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: kem_encapsulate / kem_decapsulate via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *encaps_resp, *decaps_resp;
const char *pub_hex;
char encaps_req[6000]; /* pub_hex is 2368 chars + JSON overhead */
char *decaps_req = (char *)malloc(6000);
cJSON *encaps_json, *result_item, *ct_item, *ss_item;
const char *ct_str = NULL, *ss_encaps_str = NULL;
const char *ss_decaps_str = NULL;
role_table_init(&table);
pq_role = make_pq_kem_ml_kem_768_entry("pq_kem", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* Get the public key hex for encaps */
pub_hex = crypto_get_pubkey_hex(&g_key_store, 0);
check_condition("dispatcher integration: pubkey available", pub_hex != NULL);
if (pub_hex != NULL && decaps_req != NULL) {
/* kem_encapsulate request */
snprintf(encaps_req, sizeof(encaps_req),
"{\"id\":\"e1\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}",
pub_hex);
encaps_resp = dispatcher_handle_request(&dispatcher, encaps_req);
check_condition("kem_encapsulate via dispatcher returns result",
encaps_resp != NULL && response_has(encaps_resp, "\"result\""));
check_condition("kem_encapsulate result contains ciphertext",
encaps_resp != NULL && response_has(encaps_resp, "ciphertext"));
check_condition("kem_encapsulate result contains shared_secret",
encaps_resp != NULL && response_has(encaps_resp, "shared_secret"));
check_condition("kem_encapsulate result contains algorithm ml-kem-768",
encaps_resp != NULL && response_has(encaps_resp, "ml-kem-768"));
/* Extract ciphertext and shared_secret from result */
if (encaps_resp != NULL) {
encaps_json = cJSON_Parse(encaps_resp);
if (encaps_json != NULL) {
result_item = cJSON_GetObjectItemCaseSensitive(encaps_json, "result");
if (cJSON_IsString(result_item)) {
cJSON *result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj != NULL) {
ct_item = cJSON_GetObjectItemCaseSensitive(result_obj, "ciphertext");
ss_item = cJSON_GetObjectItemCaseSensitive(result_obj, "shared_secret");
if (cJSON_IsString(ct_item)) ct_str = strdup(ct_item->valuestring);
if (cJSON_IsString(ss_item)) ss_encaps_str = strdup(ss_item->valuestring);
cJSON_Delete(result_obj);
}
}
cJSON_Delete(encaps_json);
}
}
check_condition("kem_encapsulate: extracted ciphertext hex", ct_str != NULL);
check_condition("kem_encapsulate: extracted shared_secret hex", ss_encaps_str != NULL);
/* kem_decapsulate request */
if (ct_str != NULL) {
snprintf(decaps_req, 6000,
"{\"id\":\"d1\",\"method\":\"kem_decapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}",
ct_str);
decaps_resp = dispatcher_handle_request(&dispatcher, decaps_req);
check_condition("kem_decapsulate via dispatcher returns result",
decaps_resp != NULL && response_has(decaps_resp, "\"result\""));
check_condition("kem_decapsulate result contains shared_secret",
decaps_resp != NULL && response_has(decaps_resp, "shared_secret"));
/* Extract shared_secret from decaps result */
if (decaps_resp != NULL) {
cJSON *decaps_json = cJSON_Parse(decaps_resp);
if (decaps_json != NULL) {
cJSON *d_result = cJSON_GetObjectItemCaseSensitive(decaps_json, "result");
if (cJSON_IsString(d_result)) {
cJSON *d_obj = cJSON_Parse(d_result->valuestring);
if (d_obj != NULL) {
cJSON *d_ss = cJSON_GetObjectItemCaseSensitive(d_obj, "shared_secret");
if (cJSON_IsString(d_ss)) ss_decaps_str = strdup(d_ss->valuestring);
cJSON_Delete(d_obj);
}
}
cJSON_Delete(decaps_json);
}
}
/* Check shared secrets match */
if (ss_encaps_str != NULL && ss_decaps_str != NULL) {
check_condition("dispatcher encaps/decaps shared secrets match",
strcmp(ss_encaps_str, ss_decaps_str) == 0);
} else {
check_condition("dispatcher encaps/decaps shared secrets match", 0);
}
free((void *)ss_decaps_str);
free(decaps_resp);
} else {
check_condition("kem_decapsulate via dispatcher returns result", 0);
check_condition("kem_decapsulate result contains shared_secret", 0);
check_condition("dispatcher encaps/decaps shared secrets match", 0);
}
free((void *)ct_str);
free((void *)ss_encaps_str);
}
free(decaps_req);
free(encaps_resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: kem_encapsulate/kem_decapsulate on pq-kem+ml-kem-768 ---- */
{
role_entry_t pq_kem = make_pq_kem_ml_kem_768_entry("pqk", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce kem_encapsulate + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_KEM_ENCAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce kem_decapsulate + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_KEM_DECAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE);
}
/* ---- Cleanup ---- */
free(priv1); free(pub1); free(priv2); free(pub2);
free(ct1); free(ct2); free(ss1); free(ss2); free(ss3);
printf("\n%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -293,6 +324,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -300,15 +345,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -339,6 +516,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -347,10 +600,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

640
tests/test_pq_crypto.c Normal file
View File

@@ -0,0 +1,640 @@
/* Test for the Phase 1 post-quantum algorithm registry (pq_crypto.c).
*
* Verifies:
* - crypto_alg_from_role returns the correct algorithm for every valid
* (curve, purpose) combination and CRYPTO_ALG_UNKNOWN for invalid ones.
* - crypto_alg_get_sizes returns the FIPS/PQClean size constants for each
* algorithm and NULL for CRYPTO_ALG_UNKNOWN.
* - crypto_alg_to_str / crypto_alg_from_str roundtrip for all algorithms.
* - role_purpose_from_str("pq-sig") == PURPOSE_PQ_SIG
* - role_purpose_from_str("pq-kem") == PURPOSE_PQ_KEM
* - role_curve_from_str("ml-dsa-65") == CURVE_ML_DSA_65
* - role_curve_from_str("slh-dsa-128s") == CURVE_SLH_DSA_128S
* - role_curve_from_str("ml-kem-768") == CURVE_ML_KEM_768
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
role_entry_t *role_table_get_default(role_table_t *table);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>
static int g_failures = 0;
static void check_condition(const char *name, int condition) {
if (condition) {
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s\n", name);
g_failures++;
}
}
int main(void) {
const crypto_alg_sizes_t *sz;
crypto_alg_t alg;
const char *str;
crypto_alg_t back;
/* ---- crypto_alg_from_role: valid combinations ---- */
check_condition("secp256k1+nostr -> SECP256K1",
crypto_alg_from_role(CURVE_SECP256K1, PURPOSE_NOSTR) == CRYPTO_ALG_SECP256K1);
check_condition("ed25519+ssh -> ED25519",
crypto_alg_from_role(CURVE_ED25519, PURPOSE_SSH) == CRYPTO_ALG_ED25519);
check_condition("x25519+age -> X25519",
crypto_alg_from_role(CURVE_X25519, PURPOSE_AGE) == CRYPTO_ALG_X25519);
check_condition("ml-dsa-65+pq-sig -> ML_DSA_65",
crypto_alg_from_role(CURVE_ML_DSA_65, PURPOSE_PQ_SIG) == CRYPTO_ALG_ML_DSA_65);
check_condition("slh-dsa-128s+pq-sig -> SLH_DSA_128S",
crypto_alg_from_role(CURVE_SLH_DSA_128S, PURPOSE_PQ_SIG) == CRYPTO_ALG_SLH_DSA_128S);
check_condition("ml-kem-768+pq-kem -> ML_KEM_768",
crypto_alg_from_role(CURVE_ML_KEM_768, PURPOSE_PQ_KEM) == CRYPTO_ALG_ML_KEM_768);
/* ---- crypto_alg_from_role: invalid combinations ---- */
check_condition("secp256k1+ssh -> UNKNOWN",
crypto_alg_from_role(CURVE_SECP256K1, PURPOSE_SSH) == CRYPTO_ALG_UNKNOWN);
check_condition("ed25519+nostr -> UNKNOWN",
crypto_alg_from_role(CURVE_ED25519, PURPOSE_NOSTR) == CRYPTO_ALG_UNKNOWN);
check_condition("ml-dsa-65+pq-kem -> UNKNOWN (purpose mismatch)",
crypto_alg_from_role(CURVE_ML_DSA_65, PURPOSE_PQ_KEM) == CRYPTO_ALG_UNKNOWN);
check_condition("ml-kem-768+pq-sig -> UNKNOWN (purpose mismatch)",
crypto_alg_from_role(CURVE_ML_KEM_768, PURPOSE_PQ_SIG) == CRYPTO_ALG_UNKNOWN);
check_condition("unknown curve -> UNKNOWN",
crypto_alg_from_role(CURVE_UNKNOWN, PURPOSE_NOSTR) == CRYPTO_ALG_UNKNOWN);
/* ---- crypto_alg_get_sizes ---- */
sz = crypto_alg_get_sizes(CRYPTO_ALG_SECP256K1);
check_condition("sizes secp256k1 not NULL", sz != NULL);
check_condition("sizes secp256k1 priv=32 pub=32 sig=64",
sz != NULL && sz->priv_key_len == 32 && sz->pub_key_len == 32 && sz->sig_len == 64 &&
sz->ciphertext_len == 0 && sz->shared_secret_len == 0);
sz = crypto_alg_get_sizes(CRYPTO_ALG_ED25519);
check_condition("sizes ed25519 priv=32 pub=32 sig=64",
sz != NULL && sz->priv_key_len == 32 && sz->pub_key_len == 32 && sz->sig_len == 64);
sz = crypto_alg_get_sizes(CRYPTO_ALG_X25519);
check_condition("sizes x25519 priv=32 pub=32 shared=32 sig=0",
sz != NULL && sz->priv_key_len == 32 && sz->pub_key_len == 32 &&
sz->sig_len == 0 && sz->shared_secret_len == 32);
sz = crypto_alg_get_sizes(CRYPTO_ALG_ML_DSA_65);
check_condition("sizes ml-dsa-65 priv=4032 pub=1952 sig=3309",
sz != NULL && sz->priv_key_len == 4032 && sz->pub_key_len == 1952 &&
sz->sig_len == 3309 && sz->ciphertext_len == 0 && sz->shared_secret_len == 0);
sz = crypto_alg_get_sizes(CRYPTO_ALG_SLH_DSA_128S);
check_condition("sizes slh-dsa-128s priv=64 pub=32 sig=7856",
sz != NULL && sz->priv_key_len == 64 && sz->pub_key_len == 32 &&
sz->sig_len == 7856 && sz->ciphertext_len == 0 && sz->shared_secret_len == 0);
sz = crypto_alg_get_sizes(CRYPTO_ALG_ML_KEM_768);
check_condition("sizes ml-kem-768 priv=2400 pub=1184 ct=1088 shared=32",
sz != NULL && sz->priv_key_len == 2400 && sz->pub_key_len == 1184 &&
sz->sig_len == 0 && sz->ciphertext_len == 1088 && sz->shared_secret_len == 32);
sz = crypto_alg_get_sizes(CRYPTO_ALG_UNKNOWN);
check_condition("sizes UNKNOWN -> NULL", sz == NULL);
/* ---- crypto_alg_to_str / crypto_alg_from_str roundtrip ---- */
str = crypto_alg_to_str(CRYPTO_ALG_SECP256K1);
check_condition("to_str secp256k1", str != NULL && strcmp(str, "secp256k1") == 0);
str = crypto_alg_to_str(CRYPTO_ALG_ED25519);
check_condition("to_str ed25519", str != NULL && strcmp(str, "ed25519") == 0);
str = crypto_alg_to_str(CRYPTO_ALG_X25519);
check_condition("to_str x25519", str != NULL && strcmp(str, "x25519") == 0);
str = crypto_alg_to_str(CRYPTO_ALG_ML_DSA_65);
check_condition("to_str ml-dsa-65", str != NULL && strcmp(str, "ml-dsa-65") == 0);
str = crypto_alg_to_str(CRYPTO_ALG_SLH_DSA_128S);
check_condition("to_str slh-dsa-128s", str != NULL && strcmp(str, "slh-dsa-128s") == 0);
str = crypto_alg_to_str(CRYPTO_ALG_ML_KEM_768);
check_condition("to_str ml-kem-768", str != NULL && strcmp(str, "ml-kem-768") == 0);
check_condition("to_str UNKNOWN -> NULL", crypto_alg_to_str(CRYPTO_ALG_UNKNOWN) == NULL);
back = crypto_alg_from_str("secp256k1");
check_condition("from_str secp256k1", back == CRYPTO_ALG_SECP256K1);
back = crypto_alg_from_str("ed25519");
check_condition("from_str ed25519", back == CRYPTO_ALG_ED25519);
back = crypto_alg_from_str("x25519");
check_condition("from_str x25519", back == CRYPTO_ALG_X25519);
back = crypto_alg_from_str("ml-dsa-65");
check_condition("from_str ml-dsa-65", back == CRYPTO_ALG_ML_DSA_65);
back = crypto_alg_from_str("slh-dsa-128s");
check_condition("from_str slh-dsa-128s", back == CRYPTO_ALG_SLH_DSA_128S);
back = crypto_alg_from_str("ml-kem-768");
check_condition("from_str ml-kem-768", back == CRYPTO_ALG_ML_KEM_768);
back = crypto_alg_from_str("not-a-real-alg");
check_condition("from_str bogus -> UNKNOWN", back == CRYPTO_ALG_UNKNOWN);
back = crypto_alg_from_str(NULL);
check_condition("from_str NULL -> UNKNOWN", back == CRYPTO_ALG_UNKNOWN);
/* roundtrip via to_str then from_str for each algorithm */
alg = CRYPTO_ALG_SECP256K1;
check_condition("roundtrip secp256k1",
crypto_alg_from_str(crypto_alg_to_str(alg)) == alg);
alg = CRYPTO_ALG_ML_DSA_65;
check_condition("roundtrip ml-dsa-65",
crypto_alg_from_str(crypto_alg_to_str(alg)) == alg);
alg = CRYPTO_ALG_ML_KEM_768;
check_condition("roundtrip ml-kem-768",
crypto_alg_from_str(crypto_alg_to_str(alg)) == alg);
/* ---- role_purpose_from_str new values ---- */
check_condition("purpose from_str pq-sig -> PQ_SIG",
role_purpose_from_str("pq-sig") == PURPOSE_PQ_SIG);
check_condition("purpose from_str pq-kem -> PQ_KEM",
role_purpose_from_str("pq-kem") == PURPOSE_PQ_KEM);
check_condition("purpose to_str PQ_SIG -> pq-sig",
strcmp(role_purpose_to_str(PURPOSE_PQ_SIG), "pq-sig") == 0);
check_condition("purpose to_str PQ_KEM -> pq-kem",
strcmp(role_purpose_to_str(PURPOSE_PQ_KEM), "pq-kem") == 0);
/* ---- role_curve_from_str new values ---- */
check_condition("curve from_str ml-dsa-65 -> ML_DSA_65",
role_curve_from_str("ml-dsa-65") == CURVE_ML_DSA_65);
check_condition("curve from_str slh-dsa-128s -> SLH_DSA_128S",
role_curve_from_str("slh-dsa-128s") == CURVE_SLH_DSA_128S);
check_condition("curve from_str ml-kem-768 -> ML_KEM_768",
role_curve_from_str("ml-kem-768") == CURVE_ML_KEM_768);
check_condition("curve to_str ML_DSA_65 -> ml-dsa-65",
strcmp(role_curve_to_str(CURVE_ML_DSA_65), "ml-dsa-65") == 0);
check_condition("curve to_str SLH_DSA_128S -> slh-dsa-128s",
strcmp(role_curve_to_str(CURVE_SLH_DSA_128S), "slh-dsa-128s") == 0);
check_condition("curve to_str ML_KEM_768 -> ml-kem-768",
strcmp(role_curve_to_str(CURVE_ML_KEM_768), "ml-kem-768") == 0);
if (g_failures == 0) {
printf("\nALL TESTS PASSED\n");
return 0;
}
printf("\n%d TEST(S) FAILED\n", g_failures);
return 1;
}

814
tests/test_pubkey_format.c Normal file
View File

@@ -0,0 +1,814 @@
/* Test for Phase 6 get_public_key response format (dispatcher.c).
*
* Verifies:
* - secp256k1: get_public_key returns plain hex string (backward compat)
* - secp256k1 with {"format":"structured"}: returns structured JSON
* - ed25519: returns structured JSON with algorithm "ed25519"
* - ML-DSA-65: returns structured JSON with algorithm "ml-dsa-65" and large pubkey
* - ML-KEM-768: returns structured JSON with algorithm "ml-kem-768"
* - Structured result contains algorithm, public_key, and key_id fields
* - key_id is the first 16 hex chars of the public key
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG,
PURPOSE_PQ_KEM,
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
role_entry_t *role_table_get_default(role_table_t *table);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0,
CRYPTO_ALG_ED25519,
CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65,
CRYPTO_ALG_SLH_DSA_128S,
CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
typedef struct {
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char npub[128];
crypto_alg_t alg;
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
static key_store_t g_key_store;
static algorithm_key_cache_t g_alg_key_cache;
#include <nostr_core/nostr_common.h>
#include <nostr_core/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
/* Parse the dispatcher response's "result" field. If result is a JSON string
* (plain hex), returns a newly-allocated copy of the string in *out_str and
* returns 1. If result is a JSON object serialized as a string, parses it into
* *out_obj and returns 2. Returns 0 on failure. Caller frees accordingly. */
static int parse_result(const char *response, char **out_str, cJSON **out_obj) {
cJSON *root = NULL;
cJSON *result_item = NULL;
int ret = 0;
if (out_str != NULL) *out_str = NULL;
if (out_obj != NULL) *out_obj = NULL;
if (response == NULL) return 0;
root = cJSON_Parse(response);
if (root == NULL) return 0;
result_item = cJSON_GetObjectItemCaseSensitive(root, "result");
if (cJSON_IsString(result_item) && result_item->valuestring != NULL) {
/* Could be a plain hex string OR a serialized JSON object string. */
cJSON *maybe_obj = cJSON_Parse(result_item->valuestring);
if (maybe_obj != NULL && cJSON_IsObject(maybe_obj)) {
if (out_obj != NULL) {
*out_obj = maybe_obj;
maybe_obj = NULL;
ret = 2;
} else {
cJSON_Delete(maybe_obj);
ret = 0;
}
} else {
cJSON_Delete(maybe_obj);
if (out_str != NULL) {
*out_str = strdup(result_item->valuestring);
ret = 1;
}
}
}
cJSON_Delete(root);
return ret;
}
static role_entry_t make_entry(const char *name, const char *purpose_str,
const char *curve_str, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose_str, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve_str, sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
if (nostr_init() != 0) {
fprintf(stderr, "nostr_init failed\n");
return 1;
}
/* ---- secp256k1: plain hex (backward compat) ---- */
{
role_table_t table;
role_entry_t nostr_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp = NULL;
char *result_str = NULL;
cJSON *result_obj = NULL;
int pr;
role_table_init(&table);
nostr_role = make_entry("main", "nostr", "secp256k1", 0);
role_table_add(&table, &nostr_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}");
check_condition("secp256k1 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
check_condition("secp256k1 get_public_key result is a plain string (not object)",
pr == 1 && result_str != NULL);
check_condition("secp256k1 plain hex result is 64 hex chars",
result_str != NULL && strlen(result_str) == 64);
check_condition("secp256k1 plain hex result has no 'algorithm' field",
!response_has(resp, "algorithm"));
free(result_str);
cJSON_Delete(result_obj);
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- secp256k1 with format:structured ---- */
{
role_table_t table;
role_entry_t nostr_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp = NULL;
char *result_str = NULL;
cJSON *result_obj = NULL;
cJSON *alg_item = NULL;
cJSON *pk_item = NULL;
cJSON *kid_item = NULL;
int pr;
role_table_init(&table);
nostr_role = make_entry("main", "nostr", "secp256k1", 0);
role_table_add(&table, &nostr_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\",\"format\":\"structured\"}]}");
check_condition("secp256k1 structured get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
check_condition("secp256k1 structured result is a JSON object",
pr == 2 && result_obj != NULL);
if (result_obj != NULL) {
alg_item = cJSON_GetObjectItemCaseSensitive(result_obj, "algorithm");
pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key");
kid_item = cJSON_GetObjectItemCaseSensitive(result_obj, "key_id");
check_condition("secp256k1 structured has algorithm=secp256k1",
cJSON_IsString(alg_item) && strcmp(alg_item->valuestring, "secp256k1") == 0);
check_condition("secp256k1 structured has public_key (64 hex chars)",
cJSON_IsString(pk_item) && strlen(pk_item->valuestring) == 64);
check_condition("secp256k1 structured has key_id (16 hex chars)",
cJSON_IsString(kid_item) && strlen(kid_item->valuestring) == 16);
check_condition("secp256k1 key_id == first 16 chars of public_key",
cJSON_IsString(pk_item) && cJSON_IsString(kid_item) &&
strncmp(pk_item->valuestring, kid_item->valuestring, 16) == 0);
} else {
check_condition("secp256k1 structured has algorithm=secp256k1", 0);
check_condition("secp256k1 structured has public_key (64 hex chars)", 0);
check_condition("secp256k1 structured has key_id (16 hex chars)", 0);
check_condition("secp256k1 key_id == first 16 chars of public_key", 0);
}
free(result_str);
cJSON_Delete(result_obj);
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- ed25519: structured JSON ---- */
{
role_table_t table;
role_entry_t ssh_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp = NULL;
char *result_str = NULL;
cJSON *result_obj = NULL;
cJSON *alg_item = NULL;
cJSON *pk_item = NULL;
cJSON *kid_item = NULL;
int pr;
role_table_init(&table);
ssh_role = make_entry("ssh_main", "ssh", "ed25519", 0);
role_table_add(&table, &ssh_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"ssh_main\"}]}");
check_condition("ed25519 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
check_condition("ed25519 get_public_key result is a JSON object",
pr == 2 && result_obj != NULL);
if (result_obj != NULL) {
alg_item = cJSON_GetObjectItemCaseSensitive(result_obj, "algorithm");
pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key");
kid_item = cJSON_GetObjectItemCaseSensitive(result_obj, "key_id");
check_condition("ed25519 structured has algorithm=ed25519",
cJSON_IsString(alg_item) && strcmp(alg_item->valuestring, "ed25519") == 0);
check_condition("ed25519 structured has public_key (64 hex chars)",
cJSON_IsString(pk_item) && strlen(pk_item->valuestring) == 64);
check_condition("ed25519 structured has key_id (16 hex chars)",
cJSON_IsString(kid_item) && strlen(kid_item->valuestring) == 16);
check_condition("ed25519 key_id == first 16 chars of public_key",
cJSON_IsString(pk_item) && cJSON_IsString(kid_item) &&
strncmp(pk_item->valuestring, kid_item->valuestring, 16) == 0);
} else {
check_condition("ed25519 structured has algorithm=ed25519", 0);
check_condition("ed25519 structured has public_key (64 hex chars)", 0);
check_condition("ed25519 structured has key_id (16 hex chars)", 0);
check_condition("ed25519 key_id == first 16 chars of public_key", 0);
}
free(result_str);
cJSON_Delete(result_obj);
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- ML-DSA-65: structured JSON with large pubkey ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp = NULL;
char *result_str = NULL;
cJSON *result_obj = NULL;
cJSON *alg_item = NULL;
cJSON *pk_item = NULL;
cJSON *kid_item = NULL;
const crypto_alg_sizes_t *sz;
int pr;
role_table_init(&table);
pq_role = make_entry("pq_sig", "pq-sig", "ml-dsa-65", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
sz = crypto_alg_get_sizes(CRYPTO_ALG_ML_DSA_65);
check_condition("ML-DSA-65 sizes available", sz != NULL);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"pq_sig\"}]}");
check_condition("ML-DSA-65 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
check_condition("ML-DSA-65 get_public_key result is a JSON object",
pr == 2 && result_obj != NULL);
if (result_obj != NULL && sz != NULL) {
alg_item = cJSON_GetObjectItemCaseSensitive(result_obj, "algorithm");
pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key");
kid_item = cJSON_GetObjectItemCaseSensitive(result_obj, "key_id");
check_condition("ML-DSA-65 structured has algorithm=ml-dsa-65",
cJSON_IsString(alg_item) && strcmp(alg_item->valuestring, "ml-dsa-65") == 0);
check_condition("ML-DSA-65 structured has large public_key (3904 hex chars)",
cJSON_IsString(pk_item) && strlen(pk_item->valuestring) == sz->pub_key_len * 2);
check_condition("ML-DSA-65 structured has key_id (16 hex chars)",
cJSON_IsString(kid_item) && strlen(kid_item->valuestring) == 16);
check_condition("ML-DSA-65 key_id == first 16 chars of public_key",
cJSON_IsString(pk_item) && cJSON_IsString(kid_item) &&
strncmp(pk_item->valuestring, kid_item->valuestring, 16) == 0);
} else {
check_condition("ML-DSA-65 structured has algorithm=ml-dsa-65", 0);
check_condition("ML-DSA-65 structured has large public_key (3904 hex chars)", 0);
check_condition("ML-DSA-65 structured has key_id (16 hex chars)", 0);
check_condition("ML-DSA-65 key_id == first 16 chars of public_key", 0);
}
free(result_str);
cJSON_Delete(result_obj);
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- ML-KEM-768: structured JSON ---- */
{
role_table_t table;
role_entry_t kem_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp = NULL;
char *result_str = NULL;
cJSON *result_obj = NULL;
cJSON *alg_item = NULL;
cJSON *pk_item = NULL;
cJSON *kid_item = NULL;
const crypto_alg_sizes_t *sz;
int pr;
role_table_init(&table);
kem_role = make_entry("kem_main", "pq-kem", "ml-kem-768", 0);
role_table_add(&table, &kem_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
sz = crypto_alg_get_sizes(CRYPTO_ALG_ML_KEM_768);
check_condition("ML-KEM-768 sizes available", sz != NULL);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"kem_main\"}]}");
check_condition("ML-KEM-768 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
check_condition("ML-KEM-768 get_public_key result is a JSON object",
pr == 2 && result_obj != NULL);
if (result_obj != NULL && sz != NULL) {
alg_item = cJSON_GetObjectItemCaseSensitive(result_obj, "algorithm");
pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key");
kid_item = cJSON_GetObjectItemCaseSensitive(result_obj, "key_id");
check_condition("ML-KEM-768 structured has algorithm=ml-kem-768",
cJSON_IsString(alg_item) && strcmp(alg_item->valuestring, "ml-kem-768") == 0);
check_condition("ML-KEM-768 structured has public_key (2368 hex chars)",
cJSON_IsString(pk_item) && strlen(pk_item->valuestring) == sz->pub_key_len * 2);
check_condition("ML-KEM-768 structured has key_id (16 hex chars)",
cJSON_IsString(kid_item) && strlen(kid_item->valuestring) == 16);
check_condition("ML-KEM-768 key_id == first 16 chars of public_key",
cJSON_IsString(pk_item) && cJSON_IsString(kid_item) &&
strncmp(pk_item->valuestring, kid_item->valuestring, 16) == 0);
} else {
check_condition("ML-KEM-768 structured has algorithm=ml-kem-768", 0);
check_condition("ML-KEM-768 structured has public_key (2368 hex chars)", 0);
check_condition("ML-KEM-768 structured has key_id (16 hex chars)", 0);
check_condition("ML-KEM-768 key_id == first 16 chars of public_key", 0);
}
free(result_str);
cJSON_Delete(result_obj);
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -198,6 +203,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -233,6 +256,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -257,6 +282,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -290,6 +321,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -297,15 +342,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -336,6 +513,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -344,10 +597,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.

837
tests/test_slh_dsa_128s.c Normal file
View File

@@ -0,0 +1,837 @@
/* Test for Phase 4 SLH-DSA-128s (FIPS 205) post-quantum hash-based signatures.
*
* Verifies:
* - SLH-DSA-128s keygen from seed: 32-byte pub, 64-byte priv
* - SLH-DSA-128s keygen determinism: same seed -> same keypair
* - SLH-DSA-128s sign/verify roundtrip: sign a message, verify passes
* - SLH-DSA-128s sign/verify with wrong message: verify fails
* - SLH-DSA-128s sign/verify with wrong key: verify fails
* - DRBG determinism: same seed -> same randombytes output
* - Integration: derive SLH-DSA-128s key from mnemonic via crypto_derive_one,
* sign data via sign_data verb through dispatcher, verify signature
* - Enforcement: sign_data allowed on pq-sig+slh-dsa-128s
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
int mnemonic_generate(int word_count, char *out, size_t out_len);
/* from role_table.h */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG,
PURPOSE_PQ_KEM,
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
const char *selector_strerror(int err);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
typedef enum {
POLICY_SOURCE_DEFAULT = 0,
POLICY_SOURCE_PREAPPROVE,
POLICY_SOURCE_SESSION_GRANT
} policy_source_t;
typedef struct {
char caller[POLICY_CALLER_MAX_LEN];
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2
#define POLICY_NO_MATCH -3
void policy_table_init(policy_table_t *table);
void policy_init_default(policy_table_t *table, uid_t owner_uid);
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0,
CRYPTO_ALG_ED25519,
CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65,
CRYPTO_ALG_SLH_DSA_128S,
CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* NSIGNER_HEADERLESS_DECLS_END */
static key_store_t g_key_store;
static algorithm_key_cache_t g_alg_key_cache;
#include <nostr_core/nostr_common.h>
#include <nostr_core/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* SLH-DSA-128s key sizes (FIPS 205) */
#define SLH_DSA_128S_PUBKEY_BYTES 32
#define SLH_DSA_128S_PRIVKEY_BYTES 64
#define SLH_DSA_128S_SIG_BYTES 7856
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
static role_entry_t make_pq_sig_slh_dsa_128s_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "pq-sig", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "slh-dsa-128s", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_nostr_secp_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
int main(void) {
/* Test mnemonic (BIP-39 standard test vector). */
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
unsigned char seed[32];
unsigned char *priv1, *pub1, *priv2, *pub2;
unsigned char *sig;
size_t sig_len;
int rc;
/* Allocate key buffers on the heap */
priv1 = (unsigned char *)malloc(SLH_DSA_128S_PRIVKEY_BYTES);
pub1 = (unsigned char *)malloc(SLH_DSA_128S_PUBKEY_BYTES);
priv2 = (unsigned char *)malloc(SLH_DSA_128S_PRIVKEY_BYTES);
pub2 = (unsigned char *)malloc(SLH_DSA_128S_PUBKEY_BYTES);
sig = (unsigned char *)malloc(SLH_DSA_128S_SIG_BYTES);
if (!priv1 || !pub1 || !priv2 || !pub2 || !sig) {
printf("FAIL: memory allocation\n");
return 1;
}
/* ---- DRBG determinism ---- */
{
unsigned char out1[64], out2[64];
memset(seed, 0x42, 32);
pq_drbg_init(seed, 32);
rc = pq_drbg_randombytes(out1, 64);
check_condition("DRBG randombytes succeeds", rc == 0);
pq_drbg_zeroize();
memset(seed, 0x42, 32);
pq_drbg_init(seed, 32);
rc = pq_drbg_randombytes(out2, 64);
check_condition("DRBG randombytes succeeds (2nd)", rc == 0);
check_condition("DRBG determinism (same seed -> same output)",
memcmp(out1, out2, 64) == 0);
pq_drbg_zeroize();
}
/* ---- SLH-DSA-128s keygen from seed ---- */
memset(seed, 0x42, 32);
rc = crypto_slh_dsa_128s_keygen_from_seed(seed, 32, priv1, pub1);
check_condition("SLH-DSA-128s keygen from seed succeeds", rc == 0);
/* Keygen determinism: same seed -> same keypair */
memset(seed, 0x42, 32);
rc = crypto_slh_dsa_128s_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("SLH-DSA-128s keygen determinism (same seed -> same key)", rc == 0 &&
memcmp(priv1, priv2, SLH_DSA_128S_PRIVKEY_BYTES) == 0 &&
memcmp(pub1, pub2, SLH_DSA_128S_PUBKEY_BYTES) == 0);
/* Different seed -> different keypair */
memset(seed, 0x99, 32);
rc = crypto_slh_dsa_128s_keygen_from_seed(seed, 32, priv2, pub2);
check_condition("SLH-DSA-128s keygen different seed -> different key", rc == 0 &&
memcmp(pub1, pub2, SLH_DSA_128S_PUBKEY_BYTES) != 0);
/* ---- SLH-DSA-128s sign/verify roundtrip ---- */
memset(seed, 0x42, 32);
crypto_slh_dsa_128s_keygen_from_seed(seed, 32, priv1, pub1);
{
const char *msg = "hello world";
sig_len = SLH_DSA_128S_SIG_BYTES;
rc = crypto_slh_dsa_128s_sign(priv1, SLH_DSA_128S_PRIVKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, &sig_len);
check_condition("SLH-DSA-128s sign succeeds", rc == 0 && sig_len > 0);
check_condition("SLH-DSA-128s signature is 7856 bytes", sig_len == SLH_DSA_128S_SIG_BYTES);
rc = crypto_slh_dsa_128s_verify(pub1, SLH_DSA_128S_PUBKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, sig_len);
check_condition("SLH-DSA-128s verify valid signature", rc == 0);
/* Wrong message should fail */
{
const char *wrong_msg = "hello worle";
rc = crypto_slh_dsa_128s_verify(pub1, SLH_DSA_128S_PUBKEY_BYTES,
(const unsigned char *)wrong_msg, strlen(wrong_msg),
sig, sig_len);
check_condition("SLH-DSA-128s verify wrong message -> invalid", rc == 1);
}
/* Wrong key should fail */
memset(seed, 0xAB, 32);
crypto_slh_dsa_128s_keygen_from_seed(seed, 32, priv2, pub2);
rc = crypto_slh_dsa_128s_verify(pub2, SLH_DSA_128S_PUBKEY_BYTES,
(const unsigned char *)msg, strlen(msg),
sig, sig_len);
check_condition("SLH-DSA-128s verify wrong key -> invalid", rc == 1);
}
/* ---- Integration: derive SLH-DSA-128s key via crypto_derive_all ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
int derived;
role_table_init(&table);
pq_role = make_pq_sig_slh_dsa_128s_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
derived = crypto_derive_all(&g_key_store, &table, &mnemonic_state);
check_condition("crypto_derive_all derives SLH-DSA-128s key", derived == 1);
{
const char *pub_hex = crypto_get_pubkey_hex(&g_key_store, 0);
/* 32 bytes hex-encoded = 64 hex chars + null */
check_condition("SLH-DSA-128s derived pubkey hex is 64 chars",
pub_hex != NULL && strlen(pub_hex) == 64);
}
{
const unsigned char *priv = crypto_get_private_key(&g_key_store, 0);
check_condition("SLH-DSA-128s derived private key not NULL", priv != NULL);
}
/* Determinism: re-derive and check same key */
{
static key_store_t key_store2;
memset(&key_store2, 0, sizeof(key_store2));
crypto_derive_all(&key_store2, &table, &mnemonic_state);
{
const char *pub_hex1 = crypto_get_pubkey_hex(&g_key_store, 0);
const char *pub_hex2 = crypto_get_pubkey_hex(&key_store2, 0);
check_condition("SLH-DSA-128s derivation determinism (same mnemonic -> same key)",
pub_hex1 != NULL && pub_hex2 != NULL &&
strcmp(pub_hex1, pub_hex2) == 0);
}
crypto_wipe(&key_store2);
}
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" in hex */
char request[512];
role_table_init(&table);
pq_role = make_pq_sig_slh_dsa_128s_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* sign_data request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm slh-dsa-128s",
resp != NULL && response_has(resp, "slh-dsa-128s"));
free(resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: verify_signature roundtrip via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
static mnemonic_state_t mnemonic_state;
dispatcher_ctx_t dispatcher;
char *sign_resp, *verify_resp;
const char *msg_hex = "68656c6c6f20776f726c64"; /* "hello world" */
char sign_req[512];
/* SLH-DSA-128s sig is 7856 bytes = 15712 hex chars. The verify request
* needs: JSON overhead + msg_hex (22) + sig_hex (15712) + options ~50
* = ~16000 bytes. Use 20000 to be safe. */
char *verify_req = (char *)malloc(20000);
cJSON *sign_json, *result_item, *sig_item;
const char *sig_str = NULL;
role_table_init(&table);
pq_role = make_pq_sig_slh_dsa_128s_entry("pq_main", 0);
role_table_add(&table, &pq_role);
mnemonic_init(&mnemonic_state);
mnemonic_load(&mnemonic_state, mnemonic);
memset(&g_key_store, 0, sizeof(g_key_store));
alg_key_cache_init(&g_alg_key_cache);
crypto_derive_all(&g_key_store, &table, &mnemonic_state);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
sign_json = cJSON_Parse(sign_resp);
if (sign_json != NULL) {
result_item = cJSON_GetObjectItemCaseSensitive(sign_json, "result");
if (cJSON_IsString(result_item)) {
cJSON *result_obj = cJSON_Parse(result_item->valuestring);
if (result_obj != NULL) {
sig_item = cJSON_GetObjectItemCaseSensitive(result_obj, "signature");
if (cJSON_IsString(sig_item)) {
sig_str = strdup(sig_item->valuestring);
}
cJSON_Delete(result_obj);
}
}
cJSON_Delete(sign_json);
}
}
check_condition("verify roundtrip: extracted signature hex", sig_str != NULL);
/* Verify */
if (sig_str != NULL && verify_req != NULL) {
snprintf(verify_req, 20000,
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, 20000,
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
}
free(verify_req);
free(sign_resp);
crypto_wipe(&g_key_store);
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on pq-sig+slh-dsa-128s ---- */
{
role_entry_t pq_sig = make_pq_sig_slh_dsa_128s_entry("pq", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce sign_data + pq-sig/slh-dsa-128s -> OK",
enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce verify_signature + pq-sig/slh-dsa-128s -> OK",
enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK);
}
/* ---- Cleanup ---- */
free(priv1);
free(pub1);
free(priv2);
free(pub2);
free(sig);
printf("\n%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

View File

@@ -82,6 +82,8 @@ typedef enum {
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */
PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */
PURPOSE_UNKNOWN
} role_purpose_t;
@@ -90,6 +92,9 @@ typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
@@ -195,6 +200,24 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
@@ -230,6 +253,8 @@ const char *enforce_strerror(int err);
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 160
#define POLICY_MAX_ALGS 16
#define POLICY_MAX_ALGS 16
/* Prompt behavior */
typedef enum {
@@ -254,6 +279,12 @@ typedef struct {
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
/* Algorithm-based (new) */
char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */
int alg_count;
int index_min; /* -1 = any */
int index_max; /* -1 = any */
/* Common */
prompt_mode_t prompt;
policy_source_t source;
} policy_entry_t;
@@ -287,6 +318,20 @@ int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Check whether caller_id is allowed to invoke `verb` with the given
* algorithm and index (algorithm-based policy). Returns POLICY_ALLOW,
* POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */
int policy_check_algorithm(const policy_table_t *table, const char *caller_id,
const char *verb, const char *algorithm, int index,
policy_source_t *out_source);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
@@ -294,15 +339,147 @@ prompt_mode_t prompt_mode_from_str(const char *s);
const char *prompt_mode_to_str(prompt_mode_t m);
/* from pq_crypto.h */
/* Algorithm identifiers */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */
CRYPTO_ALG_ED25519, /* new, SSH signatures */
CRYPTO_ALG_X25519, /* new, key agreement */
CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */
CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */
CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
/* Key sizes for each algorithm (compile-time constants) */
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len; /* 0 for KEM */
size_t ciphertext_len; /* 0 for signatures */
size_t shared_secret_len; /* 0 for signatures */
} crypto_alg_sizes_t;
/* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
/* Map role_curve_t + role_purpose_t to crypto_alg_t.
* Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
/* Convert crypto_alg_t to string. Returns NULL for unknown. */
const char *crypto_alg_to_str(crypto_alg_t alg);
/* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */
crypto_alg_t crypto_alg_from_str(const char *s);
/* ed25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ed25519: sign a message. priv is 32-byte private key.
* sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */
int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ed25519: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* x25519: derive keypair from a 32-byte seed.
* priv_out and pub_out must be at least 32 bytes each.
* Returns 0 on success, -1 on error. */
int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* x25519: derive shared secret from our private key and peer's public key.
* shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len,
const unsigned char *peer_pub, size_t pub_len,
unsigned char *shared_out, size_t *shared_out_len);
/* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010).
* seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path,
unsigned char *seed_out, size_t seed_out_len);
/* ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 4032 bytes, pub_out at least 1952 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-DSA-65: sign a message. priv is 4032-byte private key.
* sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* ML-DSA-65: verify a signature. pub is 1952-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 64 bytes, pub_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* SLH-DSA-128s: sign a message. priv is 64-byte private key.
* sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */
int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* SLH-DSA-128s: verify a signature. pub is 32-byte public key.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ML-KEM-768: generate keypair from a 32-byte seed (deterministic).
* priv_out must be at least 2400 bytes, pub_out at least 1184 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len,
unsigned char *priv_out, unsigned char *pub_out);
/* ML-KEM-768: encapsulate. pub is 1184-byte public key.
* ct_out must be at least 1088 bytes, ss_out at least 32 bytes.
* Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len,
unsigned char *ct_out, unsigned char *ss_out);
/* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext.
* ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */
int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len,
const unsigned char *ct, size_t ct_len,
unsigned char *ss_out);
/* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
int pq_drbg_randombytes(unsigned char *buf, size_t len);
void pq_drbg_zeroize(void);
/* from crypto.h */
/* Per-role derived key material (stored in secure memory) */
typedef struct {
secure_buf_t private_key; /* 32 bytes, mlock'd */
unsigned char public_key[32];
char pubkey_hex[65]; /* 64 hex chars + null */
char npub[128]; /* bech32 npub */
secure_buf_t private_key; /* mlock'd, variable size per algorithm */
secure_buf_t public_key; /* mlock'd, variable size per algorithm */
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg; /* which algorithm this key was derived for */
int valid;
} derived_key_t;
@@ -333,6 +510,82 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
void crypto_wipe(key_store_t *store);
/* from alg_api.h */
/* Check whether a verb is valid for an algorithm (algorithm-based enforcement).
* Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB.
* Does NOT check purpose — purpose is irrelevant for the new verbs. */
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg);
/* secp256k1 Schnorr (BIP-340) sign arbitrary bytes.
* priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes.
* Hashes the message with SHA-256 before signing (like Nostr event signing).
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 Schnorr (BIP-340) verify.
* pub is 32-byte x-only pubkey, sig is 64 bytes.
* Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* secp256k1 ECDSA sign arbitrary bytes.
* priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s).
* Hashes the message with SHA-256 before signing.
* Returns 0 on success, -1 on error. */
int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len,
const unsigned char *msg, size_t msg_len,
unsigned char *sig_out, size_t *sig_out_len);
/* secp256k1 ECDSA verify.
* pub is 32-byte x-only pubkey (converted internally to compressed form).
* sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */
int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len,
const unsigned char *msg, size_t msg_len,
const unsigned char *sig, size_t sig_len);
/* ---- Algorithm key cache ----
* On-demand key derivation by algorithm+index, separate from the role-based
* key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory.
* When full, the oldest entry is evicted (FIFO). */
#define ALG_KEY_CACHE_MAX 32
typedef struct {
crypto_alg_t alg;
int index;
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192];
char key_id[17];
int valid;
} alg_key_entry_t;
typedef struct {
alg_key_entry_t entries[ALG_KEY_CACHE_MAX];
int count;
} algorithm_key_cache_t;
/* Initialize an empty cache. */
void alg_key_cache_init(algorithm_key_cache_t *cache);
/* Zeroize and free all entries. Idempotent. */
void alg_key_cache_wipe(algorithm_key_cache_t *cache);
/* Look up a cached entry by (alg, index). Returns NULL if not present. */
const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index);
/* Derive a key on-demand by (alg, index) and store it in the cache.
* Uses the standard derivation path for the algorithm.
* Returns 0 on success, -1 on error. */
int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index);
/* from dispatcher.h */
@@ -341,10 +594,11 @@ typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store, algorithm_key_cache_t *alg_key_cache);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.