Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44a87432b7 | ||
|
|
6aa42d4387 | ||
|
|
0b1c3ea382 | ||
|
|
fd895db0c9 | ||
|
|
e4087441a5 | ||
|
|
5a16c4e1bf | ||
|
|
9afbb8fcbd | ||
|
|
fa98de12e4 | ||
|
|
e459e98beb | ||
|
|
32151b3ffd | ||
|
|
75dddac223 | ||
|
|
697a79dc25 | ||
|
|
e65ed5c5d6 | ||
|
|
821245ac1d | ||
|
|
7cbefe13ec | ||
|
|
d7fb3787e6 | ||
|
|
f0e90e0ea6 | ||
|
|
86a97aee01 | ||
|
|
d7eb6b5ec0 | ||
|
|
e04196f1f1 | ||
|
|
0ea3e60391 | ||
|
|
ac2e6347a2 | ||
|
|
d7369646fb | ||
|
|
b1f8076b1d | ||
|
|
314f1c520a | ||
|
|
ea2f1cd78e | ||
|
|
9282e22b00 | ||
|
|
2ba81c4bc8 | ||
|
|
64fbd5c874 | ||
|
|
ca18e1e42d | ||
|
|
b3421c3e40 | ||
|
|
96ab9741ef | ||
|
|
2af12868e2 | ||
|
|
a0a5987ffa | ||
|
|
0b0ec5eb1a | ||
|
|
0355744103 | ||
|
|
db274ce487 | ||
|
|
56f37e092d | ||
|
|
a017dc40e0 | ||
|
|
05c055503d | ||
|
|
a7c6de2dcd | ||
|
|
16a6da817c | ||
|
|
1cf541b02d | ||
|
|
8015742e29 | ||
|
|
09f3ec2f7c | ||
|
|
5744b83288 | ||
|
|
21892c108e | ||
|
|
344add841c | ||
|
|
3e3013dde1 | ||
|
|
11d3760d7b | ||
|
|
c5f1a70658 | ||
|
|
6fd7b8ce1f | ||
|
|
1b5af2fd33 | ||
|
|
9b47883330 | ||
|
|
9a8657f663 | ||
|
|
478c3a569e |
@@ -4,3 +4,26 @@ build/
|
||||
/*.a
|
||||
!resources/
|
||||
!resources/**
|
||||
|
||||
# Exclude nested .git and bare repos inside resources/nostr_core_lib.
|
||||
# These are not needed for the build and add ~1.3 GB to the Docker context.
|
||||
resources/nostr_core_lib/.git/
|
||||
resources/nostr_core_lib/rewrite_mirror/
|
||||
resources/nostr_core_lib/verify_remote_size/
|
||||
resources/nostr_core_lib/verify_remote_size_now/
|
||||
resources/nostr_core_lib/backups/
|
||||
resources/nostr_core_lib/examples/
|
||||
resources/nostr_core_lib/tests/
|
||||
resources/nostr_core_lib/plans/
|
||||
resources/nostr_core_lib/pool.log
|
||||
resources/nostr_core_lib/Trash/
|
||||
resources/nostr_core_lib/node_modules/
|
||||
resources/nostr_core_lib/nips/
|
||||
resources/nostr_core_lib/nak/
|
||||
resources/nostr_core_lib/nostr-tools/
|
||||
resources/nostr_core_lib/libsodium/
|
||||
resources/nostr_core_lib/monocypher-4.0.2/
|
||||
resources/nostr_core_lib/tiny-AES-c/
|
||||
resources/nostr_core_lib/blossom/
|
||||
resources/nostr_core_lib/ndk/
|
||||
resources/nostr_core_lib/cline_history/
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
description: "Call n_signer to sign Nostr events, get public keys, encrypt/decrypt, and perform crypto operations across all transports (qrexec, unix socket, TCP, HTTP, USB/serial)."
|
||||
---
|
||||
|
||||
# n_signer Client Skill
|
||||
|
||||
This skill tells an agent how to call [`n_signer`](../README.md) — a hardware/software signing oracle that holds BIP-39 keys in locked memory. The signer may be running on the same machine (Unix socket), in another Qubes qube (qrexec), on a hardware device (USB/serial), or reachable over TCP/HTTP.
|
||||
|
||||
## 1. Transport overview
|
||||
|
||||
| Transport | Scope | Auth required | Best for |
|
||||
|-----------|-------|---------------|----------|
|
||||
| **qrexec** | Cross-qube (Qubes OS) | No (identity from `QREXEC_REMOTE_DOMAIN`) | Agents in caller qubes |
|
||||
| **Unix abstract socket** | Same machine | No (identity from `SO_PEERCRED`) | Local processes |
|
||||
| **TCP (FIPS mesh)** | Cross-qube or network | Yes (kind-27235 auth envelope) | Remote callers, FIPS networks |
|
||||
| **HTTP** | Cross-qube or network | Yes (kind-27235 auth envelope) | curl-friendly, REST clients |
|
||||
| **USB/serial** | Hardware signer (Feather, Teensy, CYD) | No (physical possession) | Embedded/air-gap |
|
||||
| **Stdio** | One-shot via pipe | No | Scripted one-off calls |
|
||||
|
||||
## 2. Wire protocol (all transports)
|
||||
|
||||
Every request/response uses **length-prefixed framing**:
|
||||
|
||||
```
|
||||
[4-byte big-endian payload length][UTF-8 JSON payload]
|
||||
```
|
||||
|
||||
The JSON payload is a JSON-RPC 2.0-style object:
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "method": "<verb>", "params": [<arg0>, <arg1>, ..., {<options>}] }
|
||||
```
|
||||
|
||||
Response (success):
|
||||
```json
|
||||
{ "id": "<string>", "result": "<value>" }
|
||||
```
|
||||
|
||||
Response (error):
|
||||
```json
|
||||
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
## 3. Transport-specific invocation
|
||||
|
||||
### 3.1 Qubes qrexec (easiest cross-qube)
|
||||
|
||||
```bash
|
||||
# Pipe framed JSON-RPC through qrexec-client-vm
|
||||
printf '\x00\x00\x00\x3f'"$(echo '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}')" | qrexec-client-vm <signer_qube> qubes.NsignerRpc | tail -c +5
|
||||
```
|
||||
|
||||
Python (stdlib only, zero deps):
|
||||
```python
|
||||
import json, struct, subprocess
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
payload = json.dumps(request, separators=(",", ":")).encode()
|
||||
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, _ = proc.communicate(frame)
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
return json.loads(out[4:4+length])
|
||||
```
|
||||
|
||||
See [`client/demo_python.py`](../client/demo_python.py) for a full working demo (get_public_key → sign_event → nip44 → mine_event).
|
||||
|
||||
### 3.2 Local Unix abstract socket (same machine)
|
||||
|
||||
```bash
|
||||
# Find running signers
|
||||
nsigner list
|
||||
|
||||
# Connect via socat or the nsigner client subcommand
|
||||
nsigner client '<json>' --socket-name <name>
|
||||
```
|
||||
|
||||
C (using `nostr_core_lib`):
|
||||
```c
|
||||
nsigner_transport_t *t = nsigner_transport_open_unix("nsigner", 5000);
|
||||
nsigner_client_t *c = nsigner_client_new(t);
|
||||
cJSON *result = NULL;
|
||||
nsigner_client_call(c, "nostr_get_public_key", params, &result);
|
||||
```
|
||||
|
||||
See [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) and [`examples/sign_event_client.c`](../examples/sign_event_client.c).
|
||||
|
||||
### 3.3 TCP (FIPS mesh, cross-qube)
|
||||
|
||||
Requires a **kind-27235 auth envelope** (signed Nostr event proving caller identity).
|
||||
|
||||
Python (with `coincurve`):
|
||||
```python
|
||||
import hashlib, json, socket, struct, time
|
||||
from coincurve import PrivateKey
|
||||
|
||||
# Build auth envelope
|
||||
sk = PrivateKey(caller_privkey_bytes)
|
||||
pubkey_x = sk.public_key.format(compressed=False)[1:33].hex()
|
||||
body_hash = hashlib.sha256(json.dumps(params, separators=(",",":")).encode()).hexdigest()
|
||||
tags = [["nsigner_rpc","1"],["nsigner_method","get_public_key"],["nsigner_body_hash",body_hash]]
|
||||
serialized = json.dumps([0, pubkey_x, created_at, 27235, tags, content], separators=(",",":")).encode()
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00"*32).hex()
|
||||
|
||||
request = {"id":"1","method":"get_public_key","params":params,"auth":{"id":event_id,"pubkey":pubkey_x,"created_at":created_at,"kind":27235,"tags":tags,"content":"py-min","sig":sig}}
|
||||
```
|
||||
|
||||
See [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js).
|
||||
|
||||
### 3.4 HTTP listener
|
||||
|
||||
```bash
|
||||
curl -X POST http://<host>:<port>/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"id":"1","method":"get_public_key","params":[],"auth":{...}}'
|
||||
```
|
||||
|
||||
### 3.5 USB/serial (hardware signers)
|
||||
|
||||
For Feather S3, Teensy 4.1, CYD ESP32, etc. — connect over USB CDC serial with the same framing.
|
||||
|
||||
See [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and [`examples/feather_sign_event.py`](../examples/feather_sign_event.py).
|
||||
|
||||
## 4. Key verbs
|
||||
|
||||
| Verb | What it does | Params |
|
||||
|------|-------------|--------|
|
||||
| `get_public_key` | Get pubkey for algorithm+index | `[{"algorithm":"secp256k1","index":0}]` |
|
||||
| `nostr_get_public_key` | Get secp256k1 pubkey by nostr_index | `[{"nostr_index":0}]` |
|
||||
| `nostr_sign_event` | Sign a Nostr event | `["<event_json>",{"nostr_index":0}]` |
|
||||
| `sign` | Sign arbitrary bytes (any algorithm) | `["<hex>",{"algorithm":"ed25519","index":0}]` |
|
||||
| `nostr_nip44_encrypt` | NIP-44 encrypt | `["<peer_hex>","<plaintext>",{"nostr_index":0}]` |
|
||||
| `nostr_nip44_decrypt` | NIP-44 decrypt | `["<peer_hex>","<ciphertext>",{"nostr_index":0}]` |
|
||||
| `nostr_mine_event` | NIP-13 PoW mine + sign | `["<event_json>",{"nostr_index":0,"difficulty":4}]` |
|
||||
| `encapsulate` | ML-KEM-768 encapsulate | `["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]` |
|
||||
| `decapsulate` | ML-KEM-768 decapsulate | `["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]` |
|
||||
| `derive` | HMAC-SHA256(privkey, data) | `["<data>",{"algorithm":"secp256k1","index":0}]` |
|
||||
| `get_info` | Signer metadata | `[]` |
|
||||
|
||||
Full verb table: [`README.md §4.3`](../README.md#43-verbs)
|
||||
|
||||
## 5. Algorithms
|
||||
|
||||
| Algorithm | Key type | FIPS | Derivation path |
|
||||
|-----------|----------|------|-----------------|
|
||||
| `secp256k1` | Signature (Nostr) | — | `m/44'/1237'/<n>'/0/0` |
|
||||
| `ed25519` | Signature (SSH) | — | `m/44'/102001'/<n>'/0/0'` |
|
||||
| `x25519` | Key agreement (age) | — | `m/44'/102002'/<n>'/0/0'` |
|
||||
| `ml-dsa-65` | PQ signature | FIPS 204 | DRBG from seed |
|
||||
| `slh-dsa-128s` | PQ hash-based sig | FIPS 205 | DRBG from seed |
|
||||
| `ml-kem-768` | PQ KEM | FIPS 203 | DRBG from seed |
|
||||
| `otp` | One-time pad | — | USB pad, no derivation |
|
||||
|
||||
## 6. Publishing a Nostr event (end-to-end)
|
||||
|
||||
```python
|
||||
import json, struct, subprocess, time
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
payload = json.dumps(request, separators=(",", ":")).encode()
|
||||
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, _ = proc.communicate(frame)
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
return json.loads(out[4:4+length])
|
||||
|
||||
# 1. Get pubkey
|
||||
pub = call_nsigner("nostr_signer", {"id":"1","method":"get_public_key","params":[{"nostr_index":0}]})["result"]
|
||||
|
||||
# 2. Build and sign event
|
||||
event = {"kind":1,"content":"Hello from my agent!","created_at":int(time.time()),"tags":[],"pubkey":pub}
|
||||
result = call_nsigner("nostr_signer", {"id":"2","method":"nostr_sign_event","params":[json.dumps(event),{"nostr_index":0}]})
|
||||
signed = json.loads(result["result"])
|
||||
|
||||
# 3. Broadcast to relay(s)
|
||||
# signed["id"] and signed["sig"] are now populated
|
||||
```
|
||||
|
||||
## 7. Reference files
|
||||
|
||||
| File | What it shows |
|
||||
|------|---------------|
|
||||
| [`client/demo_python.py`](../client/demo_python.py) | Full Python demo (qrexec, stdlib only) |
|
||||
| [`client/demo_javascript.js`](../client/demo_javascript.js) | Full Node.js demo (qrexec) |
|
||||
| [`client/demo_c99.c`](../client/demo_c99.c) | Full C99 demo (qrexec, nostr_core_lib) |
|
||||
| [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) | Minimal TCP/FIPS with auth envelope |
|
||||
| [`examples/get_pubkey_qrexec.c`](../examples/get_pubkey_qrexec.c) | Minimal qrexec in C |
|
||||
| [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) | Minimal TCP in C with auth envelope |
|
||||
| [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) | TCP/FIPS in Node.js |
|
||||
| [`examples/n_signer_qube_example_qrexec.js`](../examples/n_signer_qube_example_qrexec.js) | qrexec in Node.js |
|
||||
| [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) | Full wire contract spec |
|
||||
| [`documents/AGENT_CLIENT.md`](../documents/AGENT_CLIENT.md) | Comprehensive agent reference |
|
||||
@@ -0,0 +1,13 @@
|
||||
alarm impact educate burden vague honey horn buyer sight vocal age render
|
||||
|
||||
index 0
|
||||
|
||||
{
|
||||
"index": 0,
|
||||
"nsec": "nsec1z2lrfamae2dzax7dmnlhv497uuxe4mw0m3w694upx5x54q6dgttqvzrrwl",
|
||||
"npub": "npub1j7d7yf47w8k2kseknqjr3045jvm00u0wnt3433kk6vu67d2zamcs8ynuw4",
|
||||
"npubHex": "979be226be71ecab4336982438beb49336f7f1ee9ae358c6d6d339af3542eef1",
|
||||
"nsecHex": "12be34f77dca9a2e9bcddcff7654bee70d9aedcfdc5da2d781350d4a834d42d6",
|
||||
"fipsIpv6": "fd55:b7c6:536e:26ee:a79:6e25:6f05:a85f",
|
||||
"strDerivationPath": "m/44'/1237'/0'/0/0"
|
||||
}
|
||||
+60
-3
@@ -55,9 +55,13 @@ RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev
|
||||
|
||||
# Copy source files
|
||||
COPY src/ /build/src/
|
||||
COPY client/ /build/client/
|
||||
COPY libotppad/ /build/libotppad/
|
||||
COPY resources/tui_continuous/ /build/resources/tui_continuous/
|
||||
COPY resources/pqclean/ /build/resources/pqclean/
|
||||
|
||||
# Build nsigner as a fully static binary
|
||||
# Also build nsigner_client as a fully static binary
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
aarch64|arm64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_arm64.a" ;; \
|
||||
@@ -70,6 +74,12 @@ 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 \
|
||||
-I/build/libotppad \
|
||||
/build/src/main.c \
|
||||
/build/src/secure_mem.c \
|
||||
/build/src/mnemonic.c \
|
||||
@@ -83,15 +93,62 @@ 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/src/otp_pad.c \
|
||||
/build/src/http_listener.c \
|
||||
/build/libotppad/libotppad.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/sign.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/poly.c \
|
||||
/build/resources/pqclean/crypto_sign/ml-dsa-65/ntt.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/sign.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/hash.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/thash.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/utils.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/wots.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/fors.c \
|
||||
/build/resources/pqclean/crypto_sign/slh-dsa-128s/address.c \
|
||||
/build/resources/pqclean/common/fips202.c \
|
||||
/build/resources/pqclean/common/sha2.c \
|
||||
/build/resources/pqclean/common/crypto_backend_openssl.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/reduce.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/ntt.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/cbd.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/verify.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/symmetric.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/poly.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/indcpa.c \
|
||||
/build/resources/pqclean/crypto_kem/ml-kem-768/kem.c \
|
||||
/build/resources/tui_continuous/tui_continuous.c \
|
||||
"$NOSTR_LIB" \
|
||||
-o /build/nsigner_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
-lsecp256k1 -lsqlite3 -lz -lpthread -lm
|
||||
|
||||
RUN strip /build/nsigner_static || true
|
||||
RUN file /build/nsigner_static && \
|
||||
(ldd /build/nsigner_static 2>&1 || true)
|
||||
# Build nsigner_client (links nostr_core_lib, much smaller than nsigner)
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
aarch64|arm64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_arm64.a" ;; \
|
||||
x86_64|amd64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_x64.a" ;; \
|
||||
*) echo "Unsupported build arch: $ARCH"; exit 1 ;; \
|
||||
esac; \
|
||||
gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
|
||||
-DNOSTR_ENABLE_NSIGNER_CLIENT=1 -D_GNU_SOURCE \
|
||||
-I/build/nostr_core_lib \
|
||||
-I/build/nostr_core_lib/nostr_core \
|
||||
-I/build/nostr_core_lib/cjson \
|
||||
/build/client/n_signer_client.c \
|
||||
"$NOSTR_LIB" \
|
||||
-o /build/nsigner_client_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
-lsecp256k1 -lz -lpthread -lm
|
||||
|
||||
RUN strip /build/nsigner_static /build/nsigner_client_static || true
|
||||
RUN file /build/nsigner_static /build/nsigner_client_static && \
|
||||
(ldd /build/nsigner_static 2>&1 || true) && \
|
||||
(ldd /build/nsigner_client_static 2>&1 || true)
|
||||
|
||||
FROM scratch AS output
|
||||
COPY --from=builder /build/nsigner_static /nsigner_static
|
||||
COPY --from=builder /build/nsigner_client_static /nsigner_client_static
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
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
|
||||
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections \
|
||||
-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE \
|
||||
-fstack-clash-protection \
|
||||
-DNOSTR_ENABLE_NSIGNER_CLIENT=1 -D_GNU_SOURCE \
|
||||
-Isrc -Ilibotppad -Iresources/nostr_core_lib \
|
||||
-Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson \
|
||||
-Iresources/tui_continuous -Iresources/pqclean \
|
||||
-Iresources/pqclean/crypto_sign/ml-dsa-65 \
|
||||
-Iresources/pqclean/crypto_sign/slh-dsa-128s \
|
||||
-Iresources/pqclean/crypto_kem/ml-kem-768 -Iresources/pqclean/common
|
||||
LDFLAGS := -Wl,--gc-sections -pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack \
|
||||
resources/nostr_core_lib/libnostr_core_x64.a \
|
||||
-lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
|
||||
SRC_DIR := src
|
||||
BUILD_DIR := build
|
||||
@@ -10,6 +21,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 +60,13 @@ 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 \
|
||||
$(SRC_DIR)/otp_pad.c \
|
||||
$(SRC_DIR)/http_listener.c \
|
||||
libotppad/libotppad.c \
|
||||
$(PQCLEAN_SOURCES) \
|
||||
resources/tui_continuous/tui_continuous.c
|
||||
|
||||
HEADERS :=
|
||||
@@ -40,18 +83,59 @@ 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
|
||||
TEST_PATH_WHITELIST_TARGET := $(BUILD_DIR)/test_path_whitelist
|
||||
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
|
||||
N_SIGNER_CLIENT_TARGET := $(BUILD_DIR)/nsigner_client
|
||||
|
||||
.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 test-path-whitelist test-n-signer-client examples clients test-client clean
|
||||
|
||||
all: dev
|
||||
# Guard for non-static build targets.
|
||||
# The canonical build is `make static` (runs build_static.sh).
|
||||
# To use dev/test targets, set NSIGNER_ALLOW_DEV_BUILD=1 in your environment.
|
||||
# This prevents AI agents from accidentally using the wrong build path.
|
||||
define BUILD_GUARD
|
||||
@if [ -z "$$NSIGNER_ALLOW_DEV_BUILD" ]; then \
|
||||
echo "=========================================================="; \
|
||||
echo "ERROR: This target is blocked for non-interactive agents."; \
|
||||
echo " For testing and deployment, use:"; \
|
||||
echo ""; \
|
||||
echo " ./build_static.sh"; \
|
||||
echo " or"; \
|
||||
echo " make static"; \
|
||||
echo ""; \
|
||||
echo " The static build produces the canonical binary that"; \
|
||||
echo " matches production deployments."; \
|
||||
echo ""; \
|
||||
echo " To override (human developers only):"; \
|
||||
echo " NSIGNER_ALLOW_DEV_BUILD=1 make <target>"; \
|
||||
echo "=========================================================="; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
|
||||
all: dev clients
|
||||
|
||||
lib:
|
||||
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
|
||||
$(BUILD_GUARD)
|
||||
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,13,19,44
|
||||
|
||||
dev: lib $(TARGET_DEV)
|
||||
$(BUILD_GUARD)
|
||||
|
||||
$(TARGET_DEV): $(SOURCES)
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -72,7 +156,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-path-whitelist test-n-signer-client test-client
|
||||
|
||||
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_INTEGRATION_TARGET)
|
||||
@@ -107,13 +191,49 @@ test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
|
||||
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_QREXEC_AUTH_TARGET)
|
||||
|
||||
test-client: examples
|
||||
test-mine-event: $(TEST_MINE_EVENT_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_MINE_EVENT_TARGET)
|
||||
|
||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_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-path-whitelist: $(TEST_PATH_WHITELIST_TARGET)
|
||||
./$(TEST_PATH_WHITELIST_TARGET)
|
||||
|
||||
test-n-signer-client: clients $(TARGET_DEV)
|
||||
./$(TEST_DIR)/test_n_signer_client.sh
|
||||
|
||||
test-client: examples clients
|
||||
|
||||
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)
|
||||
|
||||
clients: $(N_SIGNER_CLIENT_TARGET)
|
||||
|
||||
$(N_SIGNER_CLIENT_TARGET): $(CLIENT_DIR)/n_signer_client.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(CLIENT_DIR)/n_signer_client.c -o $(N_SIGNER_CLIENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -127,13 +247,13 @@ $(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c -o $(TEST_SELECTOR_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c
|
||||
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/pq_crypto.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -155,6 +275,42 @@ $(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 $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c -o $(TEST_PQ_CRYPTO_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ED25519_X25519_TARGET): $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ML_DSA_65_TARGET): $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_SLH_DSA_128S_TARGET): $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ML_KEM_768_TARGET): $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_PUBKEY_FORMAT_TARGET): $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ALGORITHM_API_TARGET): $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ALGORITHM_API_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_PATH_WHITELIST_TARGET): $(TEST_DIR)/test_path_whitelist.c $(SRC_DIR)/server.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/auth_envelope.c $(SRC_DIR)/transport_frame.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/http_listener.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_path_whitelist.c $(SRC_DIR)/server.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/auth_envelope.c $(SRC_DIR)/transport_frame.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/http_listener.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_PATH_WHITELIST_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 +323,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)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# n_signer API
|
||||
|
||||
The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API).
|
||||
|
||||
It covers:
|
||||
|
||||
- **§4.1 Request format** — JSON-RPC 2.0-style request shape.
|
||||
- **§4.2 Response format** — success/error shapes and the full error-code table.
|
||||
- **§4.3 Verbs** — the verb table (positional params + options), the `scheme` option for secp256k1, and the enforcement matrix.
|
||||
- **§4.4 Algorithms** — the algorithm table (secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp), derivation paths, key sizes, and the OTP one-time-pad model.
|
||||
- **§4.5 Examples** — worked request/response examples for every verb.
|
||||
- **§4.6 Role-based selectors** — `nostr_index` / `role` / `role_path` for the `nostr_*` verbs.
|
||||
- **§4.7 Pre-approval** — `--preapprove` syntax for algorithm-based and Nostr verbs.
|
||||
|
||||
For the security model, transports, and operational behavior, see [`README.md`](README.md) §1–§3 and §5–§8. For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
@@ -0,0 +1,189 @@
|
||||
# n_signer Security Audit — Remediation Report
|
||||
|
||||
**Date:** 2026-08-13
|
||||
**Scope:** Full static security audit of [`src/`](../src/), [`client/`](../client/), [`libotppad/`](../libotppad/), and build configuration
|
||||
**Result:** 5 findings identified, all remediated and verified
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A comprehensive security audit of the `n_signer` codebase identified **5 security findings** across memory safety, network parsing, authentication, and build hardening. All findings have been remediated, code-reviewed, and verified against the existing test suite.
|
||||
|
||||
| Severity | Count | Status |
|
||||
|----------|-------|--------|
|
||||
| High | 1 | ✅ Remediated |
|
||||
| Medium | 3 | ✅ Remediated |
|
||||
| Low | 1 | ✅ Remediated |
|
||||
| **Total** | **5** | **All Fixed** |
|
||||
|
||||
---
|
||||
|
||||
## Findings and Remediations
|
||||
|
||||
### F-001: mlock Failure Silently Degraded to Pageable Memory
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Severity** | Medium |
|
||||
| **File** | [`src/secure_mem.c`](../src/secure_mem.c):762–798 |
|
||||
| **Status** | ✅ Remediated |
|
||||
|
||||
**Problem.** [`secure_buf_alloc()`](../src/secure_mem.c:762) called `mlock()` to pin secret material (mnemonic, private keys) in RAM. On failure (e.g., `RLIMIT_MEMLOCK` exhausted, missing `CAP_IPC_LOCK`), it printed a warning and **returned success with the buffer unlocked**. No caller checked `buf->locked`, so the process continued with secrets in pageable memory — silently undermining the "crash = total wipe" and "no filesystem footprint" guarantees. An attacker with disk access after the fact could recover key material from swap.
|
||||
|
||||
**Fix.** mlock failure is now **fatal by default**. The function prints a diagnostic with `strerror(errno)` and returns `-1`, causing startup to abort. A new opt-in escape hatch, `secure_buf_allow_unlocked()`, is wired to the `--allow-unlocked-memory` CLI flag in [`src/main.c`](../src/main.c):3653 for development/container environments where `mlock` is unavailable.
|
||||
|
||||
**Files changed:**
|
||||
- [`src/secure_mem.c`](../src/secure_mem.c) — fatal-by-default logic, `secure_buf_allow_unlocked()`, added `<errno.h>`
|
||||
- [`src/main.c`](../src/main.c) — `--allow-unlocked-memory` argument parsing + declaration
|
||||
|
||||
---
|
||||
|
||||
### F-002: HTTP Content-Length Parsed with `atol()` — No Error Detection
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Severity** | Medium |
|
||||
| **File** | [`src/http_listener.c`](../src/http_listener.c):75–163 |
|
||||
| **Status** | ✅ Remediated |
|
||||
|
||||
**Problem.** The HTTP request parser read `Content-Length` using `atol()`, which has **no error detection**: it returns `0` for non-numeric input (indistinguishable from a real `0`), and silently truncates values exceeding `LONG_MAX`. The value was stored in a signed `long` and compared against a `size_t` limit, creating signed/unsigned confusion. A `Content-Length` near `LONG_MAX` could trigger a giant allocation attempt (DoS via OOM or NULL-deref crash).
|
||||
|
||||
**Fix.** Replaced `atol()` with `strtoull()` and full validation:
|
||||
- Rejects empty/non-numeric values (`endptr == p`)
|
||||
- Rejects trailing garbage (only whitespace/CR allowed after digits)
|
||||
- Rejects values exceeding `SIZE_MAX`
|
||||
- Changed `content_length` from `long` to `size_t`, eliminating signed/unsigned confusion
|
||||
- Added a `has_content_length` flag to distinguish "missing header" from "zero length"
|
||||
|
||||
**Files changed:**
|
||||
- [`src/http_listener.c`](../src/http_listener.c) — safe parsing, type fix, drain loop type fix
|
||||
|
||||
---
|
||||
|
||||
### F-003: Auth Envelope Nonce Cache Replay After Wrap
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Severity** | **High** |
|
||||
| **Files** | [`src/auth_envelope.h`](../src/auth_envelope.h), [`src/auth_envelope.c`](../src/auth_envelope.c) |
|
||||
| **Status** | ✅ Remediated |
|
||||
|
||||
**Problem.** Replay protection used a **bounded FIFO cache of 1024 event IDs** ([`AUTH_NONCE_CACHE_SIZE`](../src/auth_envelope.h):10). When full, the oldest entry was evicted (circular overwrite). An attacker who captured 1024 valid auth envelopes could replay any of them after the cache wrapped — the evicted nonce would no longer be detected as a duplicate. Combined with the 30-second timestamp skew window, this allowed impersonation of any previously-seen caller.
|
||||
|
||||
**Fix.** Replaced the bounded FIFO cache with a **hybrid per-pubkey replay tracker**:
|
||||
|
||||
1. **Monotonic timestamp per pubkey** — tracks the highest `created_at` seen for each of up to 64 pubkeys. Any envelope with `created_at < max_seen` is rejected as a replay. This has **no wrap-around weakness**.
|
||||
2. **Event ID set for the current second** — because `created_at` has 1-second granularity, a per-(pubkey, second) set of up to 32 event IDs allows multiple legitimate concurrent requests within the same second while still rejecting exact duplicates.
|
||||
3. When `created_at > max_seen`, the event ID set is cleared and the timestamp advances.
|
||||
|
||||
The initial monotonic-only version was caught by the existing test suite ([`tests/test_auth_envelope.c`](../tests/test_auth_envelope.c)) which builds multiple same-second requests — the hybrid design passes all 13 tests.
|
||||
|
||||
**Files changed:**
|
||||
- [`src/auth_envelope.h`](../src/auth_envelope.h) — new `auth_pubkey_entry_t` structure with `max_created_at` + `event_ids[]`
|
||||
- [`src/auth_envelope.c`](../src/auth_envelope.c) — new `auth_nonce_cache_check_and_update()` implementing the hybrid check; event ID extracted from the signed envelope's `id` field
|
||||
|
||||
---
|
||||
|
||||
### F-004: OTP Binary Header Checksum Parsed with `sscanf` — Return Value Ignored
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Severity** | Low |
|
||||
| **File** | [`src/otp_pad.c`](../src/otp_pad.c):408–416 |
|
||||
| **Status** | ✅ Remediated |
|
||||
|
||||
**Problem.** When building a binary `.otp` output header, the hex pad checksum was converted to bytes using `sscanf("%02x")` in a loop, but the **return value was never checked**. If the checksum string were ever malformed, `sscanf` would leave the destination variable uninitialized, producing garbage in the output header.
|
||||
|
||||
**Fix.** Added a return-value check: if `sscanf` does not return exactly 1, the function zeroizes the scratch buffer and returns an error. (An earlier version of this fix incorrectly called `free(blob)` before `blob` was declared — this was caught in code review and corrected.)
|
||||
|
||||
**Files changed:**
|
||||
- [`src/otp_pad.c`](../src/otp_pad.c) — `sscanf` return value checked with proper error cleanup
|
||||
|
||||
---
|
||||
|
||||
### F-005: Missing Compiler Hardening Flags
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Severity** | Medium |
|
||||
| **File** | [`Makefile`](../Makefile):1–10 |
|
||||
| **Status** | ✅ Remediated |
|
||||
|
||||
**Problem.** The build used only `-Wall -Wextra -Os` with no exploit-mitigation flags. The resulting binary had no stack canaries, no `_FORTIFY_SOURCE` bounds checking, no PIE (fixed load address — trivial ROP), writable GOT (no RELRO), and a potentially executable stack. For a program that parses untrusted network input while holding signing keys, these gaps significantly raise the impact of any memory-corruption bug.
|
||||
|
||||
**Fix.** Added the standard hardening flag set to `CFLAGS` and `LDFLAGS`:
|
||||
|
||||
- `-fstack-protector-strong` — stack canaries
|
||||
- `-D_FORTIFY_SOURCE=2` — compile-time + runtime bounds checking for libc functions
|
||||
- `-fPIE` / `-pie` — position-independent executable (ASLR for code)
|
||||
- `-Wl,-z,relro -Wl,-z,now` — full RELRO (read-only GOT after startup)
|
||||
- `-Wl,-z,noexecstack` — non-executable stack (NX)
|
||||
- `-fstack-clash-protection` — stack-clash probing
|
||||
|
||||
**Verification.** The rebuilt binary is confirmed as `ELF 64-bit LSB pie executable`. Notably, the new `-fstack-protector-strong` flag **immediately caught a pre-existing latent buffer overflow** in [`tests/test_selector.c`](../tests/test_selector.c) (stack smashing detected at runtime) — a bug that was previously silent. This validates the value of the hardening flags.
|
||||
|
||||
**Files changed:**
|
||||
- [`Makefile`](../Makefile) — hardening flags in `CFLAGS` and `LDFLAGS`
|
||||
|
||||
---
|
||||
|
||||
## Post-Remediation Defects Caught in Review
|
||||
|
||||
During code review of the initial fixes, 4 defects were identified and corrected before final verification:
|
||||
|
||||
| # | Defect | File | Resolution |
|
||||
|---|--------|------|-----------|
|
||||
| 1 | `errno` used without `#include <errno.h>` (compile error) | [`src/secure_mem.c`](../src/secure_mem.c) | Added include |
|
||||
| 2 | `free(blob)` referenced before `blob` was declared (compile error) | [`src/otp_pad.c`](../src/otp_pad.c) | Removed erroneous `free()`; only `secure_memzero` needed on that path |
|
||||
| 3 | Duplicated pubkey validation block (dead code) | [`src/auth_envelope.c`](../src/auth_envelope.c) | Removed duplicate |
|
||||
| 4 | `secure_buf_allow_unlocked()` not declared in main.c's headerless block (compile error) | [`src/main.c`](../src/main.c) | Added declaration |
|
||||
| 5 | Monotonic-only timestamp rejected same-second requests (test failure) | [`src/auth_envelope.c`](../src/auth_envelope.c) | Upgraded to hybrid timestamp + event-ID design |
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Build
|
||||
- Compiles cleanly with all hardening flags enabled
|
||||
- Output binary confirmed as PIE: `ELF 64-bit LSB pie executable, x86-64`
|
||||
|
||||
### Test Suite
|
||||
|
||||
| Test | Result |
|
||||
|------|--------|
|
||||
| `test_auth_envelope` | ✅ 13/13 passed (validates F-003 hybrid design) |
|
||||
| `test_mnemonic` | ✅ All passed |
|
||||
| `test_role_table` | ✅ All passed |
|
||||
| `test_enforcement` | ✅ 10/10 passed |
|
||||
| `test_policy` | ✅ 43/43 passed |
|
||||
| `test_socket_name` | ✅ All passed |
|
||||
| `test_mnemonic_input` | ✅ All passed |
|
||||
| `test_path_whitelist` | ✅ 40/41 (1 pre-existing failure, unrelated) |
|
||||
| `test_selector` | ⚠️ Stack smashing detected — **hardening caught a pre-existing latent bug** (unrelated to remediations) |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Recommendations (Non-Blocking)
|
||||
|
||||
These items were noted during the audit but are not security findings:
|
||||
|
||||
1. **Fix the latent `test_selector` buffer overflow** now exposed by `-fstack-protector-strong`. This is a pre-existing bug in the test code, not in production code.
|
||||
2. **Apply the same hardening flags to the musl-static build** in [`Dockerfile.alpine-musl`](../Dockerfile.alpine-musl) / [`build_static.sh`](../build_static.sh) (verify musl-gcc supports `-fstack-clash-protection`, GCC 8+).
|
||||
3. **Pin vendored dependency versions** (cJSON, nostr_core_lib, PQClean, secp256k1) to specific commits and track known CVEs.
|
||||
4. **Add fuzz testing** for the HTTP parser and transport frame parser.
|
||||
5. **Document the `--allow-unlocked-memory` flag** in the README security section.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed Summary
|
||||
|
||||
| File | Finding(s) |
|
||||
|------|-----------|
|
||||
| [`src/secure_mem.c`](../src/secure_mem.c) | F-001 |
|
||||
| [`src/main.c`](../src/main.c) | F-001 (flag wiring) |
|
||||
| [`src/http_listener.c`](../src/http_listener.c) | F-002 |
|
||||
| [`src/auth_envelope.h`](../src/auth_envelope.h) | F-003 |
|
||||
| [`src/auth_envelope.c`](../src/auth_envelope.c) | F-003 |
|
||||
| [`src/otp_pad.c`](../src/otp_pad.c) | F-004 |
|
||||
| [`Makefile`](../Makefile) | F-005 |
|
||||
+103
-8
@@ -1,21 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build fully static MUSL binary for nsigner using Alpine Docker
|
||||
#
|
||||
# Speed optimization: if nothing changed since the last successful build,
|
||||
# skip the Docker build entirely and reuse the existing binaries.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
HASH_FILE="$BUILD_DIR/.nsigner_build_hash"
|
||||
|
||||
TARGET_ARCH=""
|
||||
FORCE=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "ERROR: --arch requires a value"
|
||||
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
|
||||
echo "Usage: $0 [--arch <x86_64|arm64|armv7>] [--force]"
|
||||
exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
@@ -30,9 +35,13 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument '$1'"
|
||||
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
|
||||
echo "Usage: $0 [--arch <x86_64|arm64|armv7>] [--force]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -70,14 +79,17 @@ case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="nsigner_static_x86_64"
|
||||
CLIENT_NAME="nsigner_client_static_x86_64"
|
||||
;;
|
||||
arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="nsigner_static_arm64"
|
||||
CLIENT_NAME="nsigner_client_static_arm64"
|
||||
;;
|
||||
armv7)
|
||||
PLATFORM="linux/arm/v7"
|
||||
PLATFORM="linux/v7"
|
||||
OUTPUT_NAME="nsigner_static_armv7"
|
||||
CLIENT_NAME="nsigner_client_static_armv7"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported target architecture '$ARCH'"
|
||||
@@ -103,8 +115,65 @@ echo "Project root: $SCRIPT_DIR"
|
||||
echo "Dockerfile: $DOCKERFILE"
|
||||
echo "Platform: $PLATFORM"
|
||||
echo "Output: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Client: $BUILD_DIR/$CLIENT_NAME"
|
||||
echo ""
|
||||
|
||||
# ---- Change detection ----
|
||||
# Compute a hash of all files that feed into the Docker build.
|
||||
# If the hash matches the last successful build and the output binaries
|
||||
# exist, skip the Docker build entirely.
|
||||
compute_source_hash() {
|
||||
{
|
||||
# Dockerfile itself
|
||||
cat "$DOCKERFILE"
|
||||
# .dockerignore
|
||||
cat "$SCRIPT_DIR/.dockerignore" 2>/dev/null || true
|
||||
# All source files
|
||||
find "$SCRIPT_DIR/src" "$SCRIPT_DIR/client" "$SCRIPT_DIR/libotppad" \
|
||||
"$SCRIPT_DIR/resources/tui_continuous" "$SCRIPT_DIR/resources/pqclean" \
|
||||
-type f -not -path '*/.git/*' 2>/dev/null | sort | xargs cat 2>/dev/null
|
||||
# nostr_core_lib source (exclude .git, backups, bare repos, examples, tests)
|
||||
find "$SCRIPT_DIR/resources/nostr_core_lib" \
|
||||
-type f \
|
||||
-not -path '*/.git/*' \
|
||||
-not -path '*/rewrite_mirror/*' \
|
||||
-not -path '*/verify_remote_size*' \
|
||||
-not -path '*/backups/*' \
|
||||
-not -path '*/examples/*' \
|
||||
-not -path '*/tests/*' \
|
||||
-not -path '*/Trash/*' \
|
||||
-not -path '*/node_modules/*' \
|
||||
-not -path '*/nips/*' \
|
||||
-not -path '*/nak/*' \
|
||||
-not -path '*/nostr-tools/*' \
|
||||
-not -path '*/libsodium/*' \
|
||||
-not -path '*/monocypher*' \
|
||||
-not -path '*/tiny-AES-c/*' \
|
||||
-not -path '*/blossom/*' \
|
||||
-not -path '*/ndk/*' \
|
||||
-not -path '*/cline_history/*' \
|
||||
2>/dev/null | sort | xargs cat 2>/dev/null
|
||||
} | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
CURRENT_HASH="$(compute_source_hash)"
|
||||
OUTPUT_PATH="$BUILD_DIR/$OUTPUT_NAME"
|
||||
CLIENT_PATH="$BUILD_DIR/$CLIENT_NAME"
|
||||
|
||||
if [[ "$FORCE" -eq 0 ]] && \
|
||||
[[ -f "$OUTPUT_PATH" ]] && \
|
||||
[[ -f "$CLIENT_PATH" ]] && \
|
||||
[[ -f "$HASH_FILE" ]] && \
|
||||
[[ "$(cat "$HASH_FILE" 2>/dev/null)" == "$CURRENT_HASH" ]]; then
|
||||
echo "No changes detected since last successful build."
|
||||
echo "Skipping Docker build. Existing binaries:"
|
||||
echo " $OUTPUT_PATH"
|
||||
echo " $CLIENT_PATH"
|
||||
echo ""
|
||||
echo "Use --force to rebuild anyway."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$ARCH" != "$HOST_ARCH" ]; then
|
||||
echo "[0/3] Preparing buildx + QEMU for cross-architecture build"
|
||||
if ! docker buildx inspect >/dev/null 2>&1; then
|
||||
@@ -122,6 +191,10 @@ if [ "$ARCH" != "$HOST_ARCH" ]; then
|
||||
fi
|
||||
|
||||
echo "[1/3] Building builder stage from project root context"
|
||||
# Note: we no longer docker rmi before building. The buildx cache handles
|
||||
# layer reuse, and the prune at the end prevents dangling images. Removing
|
||||
# the image here forced a full --load re-export (~370MB) every time even
|
||||
# when all layers were cache hits.
|
||||
docker buildx build \
|
||||
--platform "$PLATFORM" \
|
||||
--target builder \
|
||||
@@ -130,22 +203,44 @@ docker buildx build \
|
||||
--load \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
echo "[2/3] Extracting static binary"
|
||||
echo "[2/3] Extracting static binaries"
|
||||
CONTAINER_NAME="$(docker create "$IMAGE_TAG")"
|
||||
docker cp "$CONTAINER_NAME:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME"
|
||||
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||
strip "$BUILD_DIR/$OUTPUT_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
echo "[3/3] Verifying static binary"
|
||||
docker cp "$CONTAINER_NAME:/build/nsigner_client_static" "$BUILD_DIR/$CLIENT_NAME"
|
||||
chmod +x "$BUILD_DIR/$CLIENT_NAME"
|
||||
strip "$BUILD_DIR/$CLIENT_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
echo "[3/3] Verifying static binaries"
|
||||
file "$BUILD_DIR/$OUTPUT_NAME"
|
||||
file "$BUILD_DIR/$CLIENT_NAME"
|
||||
|
||||
LDD_OUTPUT="$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)"
|
||||
echo "$LDD_OUTPUT"
|
||||
if echo "$LDD_OUTPUT" | grep -Eq "not a dynamic executable|statically linked"; then
|
||||
echo "Static check: PASS"
|
||||
echo "nsigner static check: PASS"
|
||||
else
|
||||
echo "Static check: WARNING (verify manually)"
|
||||
echo "nsigner static check: WARNING (verify manually)"
|
||||
fi
|
||||
|
||||
LDD_OUTPUT_CLIENT="$(ldd "$BUILD_DIR/$CLIENT_NAME" 2>&1 || true)"
|
||||
echo "$LDD_OUTPUT_CLIENT"
|
||||
if echo "$LDD_OUTPUT_CLIENT" | grep -Eq "not a dynamic executable|statically linked"; then
|
||||
echo "nsigner_client static check: PASS"
|
||||
else
|
||||
echo "nsigner_client static check: WARNING (verify manually)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Build complete: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Build complete:"
|
||||
echo " $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo " $BUILD_DIR/$CLIENT_NAME"
|
||||
|
||||
# Record the source hash so the next run can skip if nothing changed.
|
||||
echo "$CURRENT_HASH" > "$HASH_FILE"
|
||||
|
||||
# Prune stale build cache older than 24h to prevent unbounded cache growth
|
||||
# from repeated buildx builds. Recent layers are kept for fast rebuilds.
|
||||
docker builder prune -af --filter "until=24h" >/dev/null 2>&1 || true
|
||||
|
||||
+71
-2
@@ -22,8 +22,8 @@ for the full integration contract.
|
||||
|---|---|
|
||||
| `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` |
|
||||
| `nsigner_client_init` / `connect_unix` / `close` | `nsigner_transport_open_unix` + `nsigner_client_new` / `nsigner_client_free` |
|
||||
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "get_public_key", ...)` |
|
||||
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "sign_event", ...)` |
|
||||
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "nostr_get_public_key", ...)` |
|
||||
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "nostr_sign_event", ...)` |
|
||||
| `nsigner_client_set_auth` | `nsigner_client_set_auth` or `nostr_signer_nsigner_set_auth` |
|
||||
| `nsigner_client_request` / `request_raw` | `nsigner_client_call` (returns parsed cJSON result) |
|
||||
|
||||
@@ -36,6 +36,75 @@ 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).
|
||||
|
||||
The API has two verb families (see [`README.md`](../README.md#4-api) §4 for the full spec):
|
||||
|
||||
**Algorithm-based verbs** — the caller specifies `algorithm` and `index` in the
|
||||
options object. No role table entry is needed.
|
||||
|
||||
| Verb | Algorithms | Description |
|
||||
|---|---|---|
|
||||
| `get_public_key` | all key-deriving algorithms | Returns the derived public key (structured) |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature |
|
||||
| `encapsulate` | ml-kem-768 | KEM encapsulation with peer's public key |
|
||||
| `decapsulate` | ml-kem-768 | KEM decapsulation with derived private key |
|
||||
| `derive_shared_secret` | x25519 | ECDH key agreement |
|
||||
| `derive` | secp256k1 | `HMAC-SHA256(privkey, data)` — key-derived MAC for opaque identifiers (`index` required) |
|
||||
| `encrypt` / `decrypt` | otp | One-time pad encrypt/decrypt (`algorithm:"otp"`) |
|
||||
|
||||
**Nostr protocol verbs** — select a secp256k1 NIP-06 key via `nostr_index` (or
|
||||
`role`/`role_path`). These are role-based.
|
||||
|
||||
| Verb | Description |
|
||||
|---|---|
|
||||
| `nostr_get_public_key` | Returns the role's secp256k1 public key |
|
||||
| `nostr_sign_event` | Sign a Nostr event |
|
||||
| `nostr_mine_event` | NIP-13 PoW mining + sign |
|
||||
| `nostr_nip44_encrypt` / `nostr_nip44_decrypt` | NIP-44 encrypt/decrypt |
|
||||
| `nostr_nip04_encrypt` / `nostr_nip04_decrypt` | NIP-04 encrypt/decrypt |
|
||||
|
||||
Example: `nsigner_client_call(client, "sign", "[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]", &result)`
|
||||
|
||||
For secp256k1, the optional `scheme` parameter selects `"schnorr"` (default) or `"ecdsa"`.
|
||||
|
||||
### `get_public_key` response format
|
||||
|
||||
The algorithm-based `get_public_key` always returns a structured JSON string:
|
||||
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`.
|
||||
|
||||
The role-based `nostr_get_public_key` returns a plain 64-hex-char secp256k1
|
||||
public key by default, or the structured form with `{"format":"structured"}`.
|
||||
|
||||
Clients should parse the `result` string with `cJSON_Parse` to extract the
|
||||
`algorithm`, `public_key`, and `key_id` fields when the result is a JSON object.
|
||||
|
||||
### Key sizes
|
||||
|
||||
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | 32 B | 32 B | 64 B | — | — |
|
||||
| ed25519 | 32 B | 32 B | 64 B | — | — |
|
||||
| x25519 | 32 B | 32 B | — | — | 32 B |
|
||||
| ML-DSA-65 | 1952 B | 4032 B | 3309 B | — | — |
|
||||
| SLH-DSA-128s | 32 B | 64 B | 7856 B | — | — |
|
||||
| ML-KEM-768 | 1184 B | 2400 B | — | 1088 B | 32 B |
|
||||
|
||||
### Example clients
|
||||
|
||||
- [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) — ML-DSA-65 sign
|
||||
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768 encaps/decaps
|
||||
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519 SSH sign
|
||||
|
||||
See [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)
|
||||
section 11 for the full multi-algorithm specification, derivation paths, and
|
||||
example request/response transcripts.
|
||||
|
||||
## Why
|
||||
|
||||
Per [`plans/nsigner_integration_plan.md`](../resources/nostr_core_lib/plans/nsigner_integration_plan.md)
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* demo_c99.c — comprehensive C99 demo for connecting to a running n_signer
|
||||
* via Qubes qrexec and performing all three core operations:
|
||||
*
|
||||
* 1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
*
|
||||
* Note: nostr_mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
|
||||
* See demo_javascript.js and demo_python.py for nostr_mine_event usage examples.
|
||||
* The high-level nostr_signer API does not yet wrap nostr_mine_event.
|
||||
*
|
||||
* This uses the high-level nostr_signer API from nostr_core_lib:
|
||||
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
|
||||
* - nostr_signer_nsigner_set_nostr_index() — select key by NIP-06 index
|
||||
* - nostr_signer_get_public_key() — get pubkey
|
||||
* - nostr_signer_sign_event() — sign an event
|
||||
* - nostr_signer_nip44_encrypt() — encrypt
|
||||
* - nostr_signer_nip44_decrypt() — decrypt
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer running in the target qube with --bridge-source-trusted
|
||||
* - qubes.NsignerRpc service installed in the target qube
|
||||
* - dom0 qrexec policy allowing this qube to call the service
|
||||
*
|
||||
* Build (from n_signer repo root):
|
||||
* make examples
|
||||
*
|
||||
* Usage:
|
||||
* ./build/demo_c99 <target_qube> [nostr_index]
|
||||
* ./build/demo_c99 nostr_signer 1
|
||||
*
|
||||
* If no nostr_index is given, defaults to 0.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "nip019.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* Helper: convert 32-byte hex pubkey to bech32 npub */
|
||||
static int hex_to_npub(const char *hex, char *out_npub, size_t out_sz) {
|
||||
unsigned char bytes[32];
|
||||
int i;
|
||||
|
||||
if (strlen(hex) != 64) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < 32; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
|
||||
return -1;
|
||||
}
|
||||
bytes[i] = (unsigned char)byte;
|
||||
}
|
||||
|
||||
return nostr_key_to_bech32(bytes, "npub", out_npub);
|
||||
}
|
||||
|
||||
/* Helper: print an error with a human-readable description */
|
||||
static void print_error(const char *operation, int rc) {
|
||||
const char *desc = "unknown error";
|
||||
switch (rc) {
|
||||
case NOSTR_ERROR_INVALID_INPUT:
|
||||
desc = "invalid input";
|
||||
break;
|
||||
case NOSTR_ERROR_CRYPTO_FAILED:
|
||||
desc = "crypto operation failed";
|
||||
break;
|
||||
case NOSTR_ERROR_IO_FAILED:
|
||||
desc = "I/O failed (transport error)";
|
||||
break;
|
||||
case NOSTR_ERROR_NETWORK_FAILED:
|
||||
desc = "network failed";
|
||||
break;
|
||||
case NOSTR_ERROR_NSIGNER_POLICY_DENIED:
|
||||
desc = "policy denied (caller not approved at signer terminal)";
|
||||
break;
|
||||
case NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED:
|
||||
desc = "index not in signer's whitelist";
|
||||
break;
|
||||
default:
|
||||
/* Try to print the numeric code */
|
||||
fprintf(stderr, " %s failed: error code %d\n", operation, rc);
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, " %s failed: %s (code %d)\n", operation, desc, rc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 1: Get a public key by nostr_index.
|
||||
* Returns the hex pubkey in `out_hex` (must be 65 bytes).
|
||||
*/
|
||||
static int demo_get_public_key(nostr_signer_t *signer, int nostr_index,
|
||||
char *out_hex, size_t hex_sz) {
|
||||
char npub[128];
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 1: get_public_key (nostr_index=%d) ===\n", nostr_index);
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, out_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("get_public_key", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (hex_to_npub(out_hex, npub, sizeof(npub)) == 0) {
|
||||
printf(" pubkey hex: %s\n", out_hex);
|
||||
printf(" npub: %s\n", npub);
|
||||
} else {
|
||||
printf(" pubkey hex: %s\n", out_hex);
|
||||
printf(" (npub conversion failed)\n");
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 2: Sign a Nostr event (kind 1 text note).
|
||||
* The signed event JSON is printed.
|
||||
*/
|
||||
static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) {
|
||||
cJSON *unsigned_event = NULL;
|
||||
cJSON *signed_event = NULL;
|
||||
char *signed_json = NULL;
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===\n");
|
||||
|
||||
/* Build an unsigned Nostr event (kind 1 text note) */
|
||||
unsigned_event = cJSON_CreateObject();
|
||||
if (unsigned_event == NULL) {
|
||||
fprintf(stderr, " failed to create event JSON\n");
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
|
||||
cJSON_AddStringToObject(unsigned_event, "content", "Hello from n_signer C99 demo!");
|
||||
cJSON_AddNumberToObject(unsigned_event, "created_at", (int)time(NULL));
|
||||
|
||||
/* tags: empty array */
|
||||
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_CreateArray());
|
||||
|
||||
/* pubkey: the signer will fill this in, but we include it for completeness */
|
||||
cJSON_AddStringToObject(unsigned_event, "pubkey", pubkey_hex);
|
||||
|
||||
printf(" Unsigned event:\n");
|
||||
{
|
||||
char *tmp = cJSON_PrintUnformatted(unsigned_event);
|
||||
if (tmp) {
|
||||
printf(" %s\n", tmp);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sign it */
|
||||
rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_sign_event", rc);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print the signed event */
|
||||
signed_json = cJSON_Print(signed_event);
|
||||
if (signed_json) {
|
||||
printf(" Signed event:\n");
|
||||
printf(" %s\n", signed_json);
|
||||
free(signed_json);
|
||||
}
|
||||
|
||||
/* Extract and show the signature and event id */
|
||||
{
|
||||
cJSON *id = cJSON_GetObjectItemCaseSensitive(signed_event, "id");
|
||||
cJSON *sig = cJSON_GetObjectItemCaseSensitive(signed_event, "sig");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
printf(" event id: %s\n", id->valuestring);
|
||||
}
|
||||
if (sig && cJSON_IsString(sig)) {
|
||||
printf(" signature: %s\n", sig->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(signed_event);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Demo 3: NIP-44 encrypt and decrypt.
|
||||
* Encrypts a message to ourselves (using our own pubkey as the peer),
|
||||
* then decrypts it to verify round-trip.
|
||||
*/
|
||||
static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) {
|
||||
const char *plaintext = "Secret message from n_signer C99 demo!";
|
||||
char *ciphertext = NULL;
|
||||
char *decrypted = NULL;
|
||||
int rc;
|
||||
|
||||
printf("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===\n");
|
||||
printf(" plaintext: \"%s\"\n", plaintext);
|
||||
printf(" peer pubkey: %s (self)\n", pubkey_hex);
|
||||
|
||||
/* Encrypt */
|
||||
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &ciphertext);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_nip44_encrypt", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
printf(" ciphertext: %s\n", ciphertext);
|
||||
|
||||
/* Decrypt (using our own pubkey as the sender) */
|
||||
rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext, &decrypted);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("nostr_nostr_nip44_decrypt", rc);
|
||||
free(ciphertext);
|
||||
return rc;
|
||||
}
|
||||
|
||||
printf(" decrypted: \"%s\"\n", decrypted);
|
||||
|
||||
/* Verify round-trip */
|
||||
if (strcmp(plaintext, decrypted) == 0) {
|
||||
printf(" ✓ Round-trip verified: plaintext matches decrypted\n");
|
||||
} else {
|
||||
printf(" ✗ Round-trip FAILED: plaintext does not match decrypted\n");
|
||||
rc = NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
free(ciphertext);
|
||||
free(decrypted);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *target_qube;
|
||||
const char *service_name = "qubes.NsignerRpc";
|
||||
int nostr_index = 0;
|
||||
nostr_signer_t *signer = NULL;
|
||||
char pubkey_hex[65];
|
||||
int rc;
|
||||
|
||||
/* Ignore SIGPIPE — qrexec subprocess may close pipes abruptly */
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
||||
fprintf(stderr, "Example: %s nostr_signer 1\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
target_qube = argv[1];
|
||||
if (argc > 2) {
|
||||
nostr_index = atoi(argv[2]);
|
||||
}
|
||||
|
||||
/* Initialize the crypto subsystem */
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("=== n_signer C99 Demo ===\n");
|
||||
printf("Target qube: %s\n", target_qube);
|
||||
printf("Service: %s\n", service_name);
|
||||
printf("nostr_index: %d\n", nostr_index);
|
||||
printf("\n");
|
||||
|
||||
/* Create a high-level signer backed by qrexec transport */
|
||||
printf("Connecting to n_signer via qrexec...\n");
|
||||
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
||||
if (signer == NULL) {
|
||||
fprintf(stderr, "Failed to create qrexec signer.\n");
|
||||
fprintf(stderr, "Is qrexec-client-vm available? Is the service installed?\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
printf("Connected.\n");
|
||||
|
||||
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
||||
rc = nostr_signer_nsigner_set_nostr_index(signer, nostr_index);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
print_error("set_nostr_index", rc);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Demo 1: Get public key */
|
||||
rc = demo_get_public_key(signer, nostr_index, pubkey_hex, sizeof(pubkey_hex));
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Demo 2: Sign an event */
|
||||
rc = demo_sign_event(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Demo 3: NIP-44 encrypt/decrypt */
|
||||
rc = demo_nip44(signer, pubkey_hex);
|
||||
|
||||
cleanup:
|
||||
printf("\n=== Summary ===\n");
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
printf("All demos completed successfully.\n");
|
||||
} else {
|
||||
printf("Demo failed with error code %d.\n", rc);
|
||||
}
|
||||
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return (rc == NOSTR_SUCCESS) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* demo_javascript.js — comprehensive JavaScript demo for connecting to a
|
||||
* running n_signer via Qubes qrexec and performing all three core operations:
|
||||
*
|
||||
* 1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
*
|
||||
* Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
|
||||
* identity comes from QREXEC_REMOTE_DOMAIN on the server side.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer running in the target qube with --bridge-source-trusted
|
||||
* - qubes.NsignerRpc service installed in the target qube
|
||||
* - dom0 qrexec policy allowing this qube to call the service
|
||||
* - nostr-tools and @noble/secp256k1 npm packages installed
|
||||
*
|
||||
* Install dependencies (from n_signer repo root):
|
||||
* npm install nostr-tools @noble/secp256k1
|
||||
*
|
||||
* Usage:
|
||||
* node client/demo_javascript.js <target_qube> [nostr_index]
|
||||
* node client/demo_javascript.js nostr_signer 1
|
||||
*
|
||||
* If no nostr_index is given, defaults to 0.
|
||||
*/
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const secp = require("@noble/secp256k1");
|
||||
const { nip19 } = require("nostr-tools");
|
||||
|
||||
// @noble/secp256k1 v3 requires sync sha256/hmacSha256
|
||||
secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest());
|
||||
secp.hashes.hmacSha256 = (key, msg) =>
|
||||
new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest());
|
||||
|
||||
/**
|
||||
* Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one
|
||||
* framed response. Each call spawns a fresh qrexec-client-vm process.
|
||||
*
|
||||
* Framing: 4-byte big-endian length prefix + JSON payload.
|
||||
* No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN).
|
||||
*/
|
||||
function callNsigner(targetQube, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = Buffer.from(JSON.stringify(request), "utf8");
|
||||
const header = Buffer.alloc(4);
|
||||
header.writeUInt32BE(payload.length, 0);
|
||||
const framed = Buffer.concat([header, payload]);
|
||||
|
||||
const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
|
||||
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
||||
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`));
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
||||
reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`));
|
||||
return;
|
||||
}
|
||||
const buf = Buffer.concat(stdoutChunks);
|
||||
if (buf.length < 4) {
|
||||
reject(new Error("short response (missing frame header)"));
|
||||
return;
|
||||
}
|
||||
const len = buf.readUInt32BE(0);
|
||||
const body = buf.subarray(4, 4 + len);
|
||||
if (body.length !== len) {
|
||||
reject(new Error(`short response payload: expected ${len}, got ${body.length}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(body.toString("utf8")));
|
||||
} catch (e) {
|
||||
reject(new Error(`failed to parse response: ${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdin.write(framed);
|
||||
proc.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 1: Get a public key by nostr_index.
|
||||
*/
|
||||
async function demoGetPublicKey(targetQube, nostrIndex) {
|
||||
console.log(`\n=== Demo 1: get_public_key (nostr_index=${nostrIndex}) ===`);
|
||||
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "1",
|
||||
method: "get_public_key",
|
||||
params: [{ nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`get_public_key failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const pubkeyHex = response.result;
|
||||
const npub = nip19.npubEncode(pubkeyHex);
|
||||
console.log(` pubkey hex: ${pubkeyHex}`);
|
||||
console.log(` npub: ${npub}`);
|
||||
return pubkeyHex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 2: Sign a Nostr event (kind 1 text note).
|
||||
*/
|
||||
async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
|
||||
console.log("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===");
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
content: "Hello from n_signer JavaScript demo!",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
pubkey: pubkeyHex,
|
||||
};
|
||||
|
||||
console.log(" Unsigned event:");
|
||||
console.log(` ${JSON.stringify(unsignedEvent)}`);
|
||||
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "2",
|
||||
method: "nostr_nostr_sign_event",
|
||||
params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`nostr_sign_event failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const signedEvent = JSON.parse(response.result);
|
||||
console.log(" Signed event:");
|
||||
console.log(` ${JSON.stringify(signedEvent)}`);
|
||||
console.log(` event id: ${signedEvent.id}`);
|
||||
console.log(` signature: ${signedEvent.sig}`);
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo 3: NIP-44 encrypt and decrypt.
|
||||
* Encrypts a message to ourselves (using our own pubkey as the peer),
|
||||
* then decrypts it to verify round-trip.
|
||||
*/
|
||||
async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
|
||||
const plaintext = "Secret message from n_signer JavaScript demo!";
|
||||
|
||||
console.log("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===");
|
||||
console.log(` plaintext: "${plaintext}"`);
|
||||
console.log(` peer pubkey: ${pubkeyHex} (self)`);
|
||||
|
||||
// Encrypt
|
||||
const encResponse = await callNsigner(targetQube, {
|
||||
id: "3",
|
||||
method: "nostr_nostr_nip44_encrypt",
|
||||
params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (encResponse.error) {
|
||||
throw new Error(`nostr_nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
|
||||
}
|
||||
|
||||
const ciphertext = encResponse.result;
|
||||
console.log(` ciphertext: ${ciphertext}`);
|
||||
|
||||
// Decrypt
|
||||
const decResponse = await callNsigner(targetQube, {
|
||||
id: "4",
|
||||
method: "nostr_nostr_nip44_decrypt",
|
||||
params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }],
|
||||
});
|
||||
|
||||
if (decResponse.error) {
|
||||
throw new Error(`nostr_nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`);
|
||||
}
|
||||
|
||||
const decrypted = decResponse.result;
|
||||
console.log(` decrypted: "${decrypted}"`);
|
||||
|
||||
if (plaintext === decrypted) {
|
||||
console.log(" ✓ Round-trip verified: plaintext matches decrypted");
|
||||
} else {
|
||||
throw new Error("Round-trip FAILED: plaintext does not match decrypted");
|
||||
}
|
||||
}
|
||||
|
||||
async function demoMineEvent(targetQube, nostrIndex) {
|
||||
console.log("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---");
|
||||
|
||||
const event = {
|
||||
kind: 1,
|
||||
content: "Hello Nostr with PoW!",
|
||||
tags: [],
|
||||
};
|
||||
|
||||
console.log(" Mining with difficulty=4, threads=4, timeout_sec=30...");
|
||||
const response = await callNsigner(targetQube, {
|
||||
id: "5",
|
||||
method: "nostr_nostr_mine_event",
|
||||
params: [JSON.stringify(event), {
|
||||
difficulty: 4,
|
||||
threads: 4,
|
||||
timeout_sec: 30,
|
||||
nostr_index: nostrIndex,
|
||||
}],
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`nostr_mine_event failed: ${JSON.stringify(response.error)}`);
|
||||
}
|
||||
|
||||
const result = JSON.parse(response.result);
|
||||
console.log(` achieved_difficulty: ${result.achieved_difficulty}`);
|
||||
console.log(` target_reached: ${result.target_reached}`);
|
||||
console.log(` elapsed_sec: ${result.elapsed_sec}`);
|
||||
console.log(` attempts: ${result.attempts}`);
|
||||
|
||||
const minedEvent = JSON.parse(result.event);
|
||||
console.log(` event id: ${minedEvent.id}`);
|
||||
console.log(` nonce tag: ${JSON.stringify(minedEvent.tags[0])}`);
|
||||
|
||||
if (result.target_reached) {
|
||||
console.log(" ✓ Target difficulty reached!");
|
||||
} else {
|
||||
console.log(` (Target not reached, best effort: ${result.achieved_difficulty} bits)`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targetQube = process.argv[2] || "nostr_signer";
|
||||
const nostrIndex = parseInt(process.argv[3] || "0", 10);
|
||||
|
||||
console.log("=== n_signer JavaScript Demo ===");
|
||||
console.log(`Target qube: ${targetQube}`);
|
||||
console.log(`Service: qubes.NsignerRpc`);
|
||||
console.log(`nostr_index: ${nostrIndex}`);
|
||||
console.log("\nConnecting to n_signer via qrexec...");
|
||||
|
||||
try {
|
||||
const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex);
|
||||
await demoSignEvent(targetQube, nostrIndex, pubkeyHex);
|
||||
await demoNip44(targetQube, nostrIndex, pubkeyHex);
|
||||
await demoMineEvent(targetQube, nostrIndex);
|
||||
|
||||
console.log("\n=== Summary ===");
|
||||
console.log("All demos completed successfully.");
|
||||
} catch (e) {
|
||||
console.error("\n=== Summary ===");
|
||||
console.error(`Demo failed: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
demo_python.py — comprehensive Python demo for connecting to a running n_signer
|
||||
via Qubes qrexec and performing all three core operations:
|
||||
|
||||
1. get_public_key — retrieve a Nostr public key by nostr_index
|
||||
2. nostr_sign_event — sign a Nostr event (kind 1 text note)
|
||||
3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||
|
||||
Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
|
||||
identity comes from QREXEC_REMOTE_DOMAIN on the server side.
|
||||
|
||||
Prerequisites:
|
||||
- n_signer running in the target qube with --bridge-source-trusted
|
||||
- qubes.NsignerRpc service installed in the target qube
|
||||
- dom0 qrexec policy allowing this qube to call the service
|
||||
- Python 3 with no external dependencies (uses only stdlib)
|
||||
|
||||
Usage:
|
||||
python3 client/demo_python.py <target_qube> [nostr_index]
|
||||
python3 client/demo_python.py nostr_signer 1
|
||||
|
||||
If no nostr_index is given, defaults to 0.
|
||||
"""
|
||||
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
# --- Bech32 encoder (NIP-19 npub conversion, no external dependencies) ---
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def bech32_polymod(values):
|
||||
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
||||
chk = 1
|
||||
for v in values:
|
||||
b = chk >> 25
|
||||
chk = (chk & 0x1FFFFFF) << 5 ^ v
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((b >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_create_checksum(hrp, data):
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data):
|
||||
combined = data + bech32_create_checksum(hrp, data)
|
||||
return hrp + "1" + "".join([CHARSET[d] for d in combined])
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad and bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
return ret
|
||||
|
||||
|
||||
def hex_to_npub(pubkey_hex):
|
||||
"""Convert a 32-byte hex pubkey to bech32 npub format (NIP-19)."""
|
||||
pubkey_bytes = bytes.fromhex(pubkey_hex)
|
||||
data = convertbits(pubkey_bytes, 8, 5)
|
||||
return bech32_encode("npub", data)
|
||||
|
||||
|
||||
# --- n_signer qrexec client ---
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
"""
|
||||
Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one
|
||||
framed response. Each call spawns a fresh qrexec-client-vm process.
|
||||
|
||||
Framing: 4-byte big-endian length prefix + JSON payload.
|
||||
No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN).
|
||||
"""
|
||||
payload = json.dumps(request, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(frame)
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"qrexec-client-vm exited with code {proc.returncode}: "
|
||||
f"{err.decode('utf-8', 'replace').strip()}"
|
||||
)
|
||||
|
||||
if len(out) < 4:
|
||||
raise RuntimeError("short response (missing frame header)")
|
||||
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
body = out[4 : 4 + length]
|
||||
if len(body) != length:
|
||||
raise RuntimeError(
|
||||
f"short response payload: expected {length}, got {len(body)}"
|
||||
)
|
||||
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
|
||||
# --- Demos ---
|
||||
|
||||
def demo_get_public_key(target_qube, nostr_index):
|
||||
"""Demo 1: Get a public key by nostr_index."""
|
||||
print(f"\n=== Demo 1: get_public_key (nostr_index={nostr_index}) ===")
|
||||
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{"id": "1", "method": "get_public_key", "params": [{"nostr_index": nostr_index}]},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"get_public_key failed: {json.dumps(response['error'])}")
|
||||
|
||||
pubkey_hex = response["result"]
|
||||
npub = hex_to_npub(pubkey_hex)
|
||||
print(f" pubkey hex: {pubkey_hex}")
|
||||
print(f" npub: {npub}")
|
||||
return pubkey_hex
|
||||
|
||||
|
||||
def demo_sign_event(target_qube, nostr_index, pubkey_hex):
|
||||
"""Demo 2: Sign a Nostr event (kind 1 text note)."""
|
||||
print("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===")
|
||||
|
||||
unsigned_event = {
|
||||
"kind": 1,
|
||||
"content": "Hello from n_signer Python demo!",
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"pubkey": pubkey_hex,
|
||||
}
|
||||
|
||||
print(" Unsigned event:")
|
||||
print(f" {json.dumps(unsigned_event, separators=(',', ':'))}")
|
||||
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "2",
|
||||
"method": "nostr_nostr_sign_event",
|
||||
"params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"nostr_sign_event failed: {json.dumps(response['error'])}")
|
||||
|
||||
signed_event = json.loads(response["result"])
|
||||
print(" Signed event:")
|
||||
print(f" {json.dumps(signed_event, separators=(',', ':'))}")
|
||||
print(f" event id: {signed_event['id']}")
|
||||
print(f" signature: {signed_event['sig']}")
|
||||
return signed_event
|
||||
|
||||
|
||||
def demo_nip44(target_qube, nostr_index, pubkey_hex):
|
||||
"""Demo 3: NIP-44 encrypt and decrypt."""
|
||||
plaintext = "Secret message from n_signer Python demo!"
|
||||
|
||||
print("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===")
|
||||
print(f' plaintext: "{plaintext}"')
|
||||
print(f" peer pubkey: {pubkey_hex} (self)")
|
||||
|
||||
# Encrypt
|
||||
enc_response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "3",
|
||||
"method": "nostr_nostr_nip44_encrypt",
|
||||
"params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in enc_response:
|
||||
raise RuntimeError(f"nostr_nip44_encrypt failed: {json.dumps(enc_response['error'])}")
|
||||
|
||||
ciphertext = enc_response["result"]
|
||||
print(f" ciphertext: {ciphertext}")
|
||||
|
||||
# Decrypt
|
||||
dec_response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "4",
|
||||
"method": "nostr_nostr_nip44_decrypt",
|
||||
"params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in dec_response:
|
||||
raise RuntimeError(f"nostr_nip44_decrypt failed: {json.dumps(dec_response['error'])}")
|
||||
|
||||
decrypted = dec_response["result"]
|
||||
print(f' decrypted: "{decrypted}"')
|
||||
|
||||
if plaintext == decrypted:
|
||||
print(" ✓ Round-trip verified: plaintext matches decrypted")
|
||||
else:
|
||||
raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")
|
||||
|
||||
|
||||
def demo_nostr_mine_event(target_qube, nostr_index):
|
||||
print("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---")
|
||||
|
||||
event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []}
|
||||
|
||||
print(" Mining with difficulty=4, threads=4, timeout_sec=30...")
|
||||
response = call_nsigner(
|
||||
target_qube,
|
||||
{
|
||||
"id": "5",
|
||||
"method": "nostr_nostr_mine_event",
|
||||
"params": [json.dumps(event), {
|
||||
"difficulty": 4,
|
||||
"threads": 4,
|
||||
"timeout_sec": 30,
|
||||
"nostr_index": nostr_index,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"nostr_mine_event failed: {json.dumps(response['error'])}")
|
||||
|
||||
result = json.loads(response["result"])
|
||||
print(f" achieved_difficulty: {result['achieved_difficulty']}")
|
||||
print(f" target_reached: {result['target_reached']}")
|
||||
print(f" elapsed_sec: {result['elapsed_sec']}")
|
||||
print(f" attempts: {result['attempts']}")
|
||||
|
||||
mined_event = json.loads(result["event"])
|
||||
print(f" event id: {mined_event['id']}")
|
||||
print(f" nonce tag: {mined_event['tags'][0]}")
|
||||
|
||||
if result["target_reached"]:
|
||||
print(" ✓ Target difficulty reached!")
|
||||
else:
|
||||
print(f" (Target not reached, best effort: {result['achieved_difficulty']} bits)")
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
target_qube = sys.argv[1] if len(sys.argv) > 1 else "nostr_signer"
|
||||
nostr_index = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
|
||||
print("=== n_signer Python Demo ===")
|
||||
print(f"Target qube: {target_qube}")
|
||||
print(f"Service: qubes.NsignerRpc")
|
||||
print(f"nostr_index: {nostr_index}")
|
||||
print("\nConnecting to n_signer via qrexec...")
|
||||
|
||||
try:
|
||||
pubkey_hex = demo_get_public_key(target_qube, nostr_index)
|
||||
demo_sign_event(target_qube, nostr_index, pubkey_hex)
|
||||
demo_nip44(target_qube, nostr_index, pubkey_hex)
|
||||
demo_nostr_mine_event(target_qube, nostr_index)
|
||||
|
||||
print("\n=== Summary ===")
|
||||
print("All demos completed successfully.")
|
||||
except Exception as e:
|
||||
print("\n=== Summary ===")
|
||||
print(f"Demo failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,855 @@
|
||||
/*
|
||||
* n_signer_client.c — standalone Linux CLI for n_signer JSON-RPC API.
|
||||
*
|
||||
* Connects to a running n_signer process over its abstract UNIX socket
|
||||
* (or TCP/serial/qrexec) and exposes the full verb surface over stdin/stdout
|
||||
* so that signed events can be piped directly into `nak publish`.
|
||||
*
|
||||
* This client uses the high-level nostr_signer_t API from nostr_core_lib
|
||||
* for all typed verbs. The per-verb cJSON-building logic lives in the
|
||||
* library, not here. The CLI is mostly argv parsing + result printing.
|
||||
*
|
||||
* Build: make clients
|
||||
* Usage: n_signer_client [global options] <verb> [verb args...]
|
||||
*
|
||||
* See client/n_signer_client_README.md for full documentation.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void print_usage(FILE *fp, const char *prog) {
|
||||
fprintf(fp,
|
||||
"Usage: %s [global options] <verb> [verb args...]\n"
|
||||
"\n"
|
||||
"Global options:\n"
|
||||
" -n, --socket-name <name> Abstract socket name (default: auto-discover)\n"
|
||||
" --timeout <ms> Transport timeout (default 5000)\n"
|
||||
" --tcp <host:port> TCP transport (requires --auth-privkey)\n"
|
||||
" --serial <device> USB CDC-ACM serial transport\n"
|
||||
" --qrexec <qube:svc> Qubes qrexec transport\n"
|
||||
" --auth-privkey <hex> Auth envelope privkey (32 bytes hex)\n"
|
||||
" --auth-label <text> Auth envelope label\n"
|
||||
"\n"
|
||||
"Selector options (for nostr_* verbs):\n"
|
||||
" --role <name> Named path-role\n"
|
||||
" --path <path> Full BIP-44 derivation path\n"
|
||||
"\n"
|
||||
"Algorithm options (for algorithm-based verbs):\n"
|
||||
" -a, --algorithm <alg> secp256k1/ed25519/x25519/ml-dsa-65/\n"
|
||||
" slh-dsa-128s/ml-kem-768/otp\n"
|
||||
" --scheme <schnorr|ecdsa> secp256k1 sign/verify scheme (default schnorr)\n"
|
||||
" --encoding <ascii|binary> OTP encoding (default ascii)\n"
|
||||
" --format <plain|structured> get-public-key output (default plain)\n"
|
||||
" --index <N> Algorithm derivation index\n"
|
||||
"\n"
|
||||
"Mine-event options:\n"
|
||||
" --difficulty <N> Target leading zero bits\n"
|
||||
" --threads <N> Mining threads (default 1)\n"
|
||||
" --timeout-sec <N> Mining timeout in seconds\n"
|
||||
"\n"
|
||||
"Verbs:\n"
|
||||
" list List running n_signer sockets\n"
|
||||
" get-info\n"
|
||||
" get-public-key\n"
|
||||
" sign-event\n"
|
||||
" mine-event\n"
|
||||
" nip04-encrypt <peer-pubkey>\n"
|
||||
" nip04-decrypt <peer-pubkey>\n"
|
||||
" nip44-encrypt <peer-pubkey>\n"
|
||||
" nip44-decrypt <peer-pubkey>\n"
|
||||
" sign <msg-hex>\n"
|
||||
" verify <msg-hex> <sig-hex>\n"
|
||||
" derive <data>\n"
|
||||
" encapsulate <peer-pubkey-hex>\n"
|
||||
" decapsulate <ciphertext-hex>\n"
|
||||
" derive-shared-secret <peer-pubkey-hex>\n"
|
||||
" encrypt <plaintext>\n"
|
||||
" decrypt <ciphertext>\n"
|
||||
" call <method>\n"
|
||||
"\n"
|
||||
"Examples:\n"
|
||||
" # List running n_signer sockets\n"
|
||||
" %s list\n"
|
||||
"\n"
|
||||
" # Get a Nostr public key by role and path\n"
|
||||
" %s --role main --path \"m/44'/1237'/0'/0/0\" get-public-key\n"
|
||||
"\n"
|
||||
" # Get a key by named path-role\n"
|
||||
" %s --role role1 --path \"m/44'/1237'/1'/1/0\" get-public-key\n"
|
||||
"\n"
|
||||
" # Sign a Nostr event from stdin and pipe to nak for publishing\n"
|
||||
" echo '{\"kind\":1,\"content\":\"hello world\",\"tags\":[],\"created_at\":1700000000}' \\\n"
|
||||
" | %s --role main --path \"m/44'/1237'/0'/0/0\" sign-event | nak publish\n"
|
||||
"\n"
|
||||
" # Sign an event from argv\n"
|
||||
" %s --role main --path \"m/44'/1237'/0'/0/0\" sign-event '{\"kind\":1,\"content\":\"hi\",\"tags\":[],\"created_at\":1700000000}'\n"
|
||||
"\n"
|
||||
" # Mine an event with proof-of-work (difficulty 20)\n"
|
||||
" %s --role main --path \"m/44'/1237'/0'/0/0\" --difficulty 20 mine-event '{\"kind\":1,\"content\":\"mined\",\"tags\":[],\"created_at\":1700000000}'\n"
|
||||
"\n"
|
||||
" # NIP-44 encrypt then decrypt a round-trip\n"
|
||||
" %s --role main --path \"m/44'/1237'/0'/0/0\" nip44-encrypt <peer-pubkey> 'secret message'\n"
|
||||
" %s --role main --path \"m/44'/1237'/0'/0/0\" nip44-decrypt <peer-pubkey> '<ciphertext>'\n"
|
||||
"\n"
|
||||
" # Ed25519 sign (SSH-style)\n"
|
||||
" %s --algorithm ed25519 --index 0 sign 68656c6c6f\n"
|
||||
"\n"
|
||||
" # Get signer metadata\n"
|
||||
" %s get-info\n"
|
||||
"\n"
|
||||
" # Qubes qrexec: get the first pubkey from a signer in the nostr_signer qube\n"
|
||||
" %s --qrexec nostr_signer:qubes.NsignerRpc --role nostr_range --path \"m/44'/1237'/0'/0/0\" get-public-key\n",
|
||||
prog, prog, prog, prog, prog, prog, prog, prog, prog, prog, prog, prog);
|
||||
}
|
||||
|
||||
/* Read one line from stdin (newline stripped). Returns malloc'd string or NULL on EOF/error. */
|
||||
static char *read_stdin_line(void) {
|
||||
size_t cap = 4096;
|
||||
size_t len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) return NULL;
|
||||
|
||||
int c;
|
||||
while ((c = fgetc(stdin)) != EOF && c != '\n') {
|
||||
if (len + 1 >= cap) {
|
||||
cap *= 2;
|
||||
char *tmp = realloc(buf, cap);
|
||||
if (!tmp) { free(buf); return NULL; }
|
||||
buf = tmp;
|
||||
}
|
||||
buf[len++] = (char)c;
|
||||
}
|
||||
if (len == 0 && c == EOF) { free(buf); return NULL; }
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Convert a hex string to raw bytes. Returns number of bytes written, or -1 on error. */
|
||||
static int hex_to_bytes(const char *hex, unsigned char *out, size_t out_sz) {
|
||||
size_t len = strlen(hex);
|
||||
if (len % 2 != 0 || len / 2 > out_sz) return -1;
|
||||
for (size_t i = 0; i < len / 2; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) return -1;
|
||||
out[i] = (unsigned char)byte;
|
||||
}
|
||||
return (int)(len / 2);
|
||||
}
|
||||
|
||||
/* Parse "host:port" string. Returns 0 on success. */
|
||||
static int parse_host_port(const char *s, char **out_host, int *out_port) {
|
||||
const char *colon = strrchr(s, ':');
|
||||
if (!colon || colon == s) return -1;
|
||||
size_t host_len = (size_t)(colon - s);
|
||||
*out_host = malloc(host_len + 1);
|
||||
if (!*out_host) return -1;
|
||||
memcpy(*out_host, s, host_len);
|
||||
(*out_host)[host_len] = '\0';
|
||||
char *end = NULL;
|
||||
long p = strtol(colon + 1, &end, 10);
|
||||
if (end == colon + 1 || *end != '\0' || p < 1 || p > 65535) {
|
||||
free(*out_host);
|
||||
*out_host = NULL;
|
||||
return -1;
|
||||
}
|
||||
*out_port = (int)p;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse "qube:service" string. Returns 0 on success. */
|
||||
static int parse_qube_service(const char *s, char **out_qube, char **out_service) {
|
||||
const char *colon = strchr(s, ':');
|
||||
if (!colon || colon == s) return -1;
|
||||
size_t qube_len = (size_t)(colon - s);
|
||||
*out_qube = malloc(qube_len + 1);
|
||||
if (!*out_qube) return -1;
|
||||
memcpy(*out_qube, s, qube_len);
|
||||
(*out_qube)[qube_len] = '\0';
|
||||
*out_service = strdup(colon + 1);
|
||||
if (!*out_service) { free(*out_qube); *out_qube = NULL; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Result printing helper */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Print a raw string result (from the *_result_json_out wrappers). */
|
||||
static void print_result_str(const char *s) {
|
||||
if (s) {
|
||||
printf("%s\n", s);
|
||||
} else {
|
||||
printf("null\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Transport setup helper */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Opens a transport based on the CLI args. Returns 0 on success.
|
||||
* On success, *out_transport is set (caller must not free if handed to signer). */
|
||||
static int open_transport(const char *socket_name, int timeout_ms,
|
||||
const char *tcp_arg, const char *serial_arg,
|
||||
const char *qrexec_arg,
|
||||
const char *auth_privkey_hex,
|
||||
nsigner_transport_t **out_transport) {
|
||||
int transport_count = (tcp_arg ? 1 : 0) + (serial_arg ? 1 : 0) + (qrexec_arg ? 1 : 0) + (socket_name ? 1 : 0);
|
||||
if (transport_count > 1) {
|
||||
fprintf(stderr, "error: --tcp, --serial, --qrexec, and --socket-name are mutually exclusive\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_transport = NULL;
|
||||
|
||||
if (tcp_arg) {
|
||||
if (!auth_privkey_hex) {
|
||||
fprintf(stderr, "error: --tcp requires --auth-privkey\n");
|
||||
return -1;
|
||||
}
|
||||
char *host = NULL;
|
||||
int port = 0;
|
||||
if (parse_host_port(tcp_arg, &host, &port) != 0) {
|
||||
fprintf(stderr, "error: invalid --tcp format (expected host:port)\n");
|
||||
return -1;
|
||||
}
|
||||
*out_transport = nsigner_transport_open_tcp(host, port, timeout_ms);
|
||||
free(host);
|
||||
if (!*out_transport) {
|
||||
fprintf(stderr, "error: cannot open TCP transport to %s\n", tcp_arg);
|
||||
return -1;
|
||||
}
|
||||
} else if (serial_arg) {
|
||||
*out_transport = nsigner_transport_open_serial(serial_arg, timeout_ms);
|
||||
if (!*out_transport) {
|
||||
fprintf(stderr, "error: cannot open serial transport on %s\n", serial_arg);
|
||||
return -1;
|
||||
}
|
||||
} else if (qrexec_arg) {
|
||||
char *qube = NULL, *service = NULL;
|
||||
if (parse_qube_service(qrexec_arg, &qube, &service) != 0) {
|
||||
fprintf(stderr, "error: invalid --qrexec format (expected qube:service)\n");
|
||||
return -1;
|
||||
}
|
||||
*out_transport = nsigner_transport_open_qrexec(qube, service, timeout_ms);
|
||||
free(qube);
|
||||
free(service);
|
||||
if (!*out_transport) {
|
||||
fprintf(stderr, "error: cannot open qrexec transport to %s\n", qrexec_arg);
|
||||
return -1;
|
||||
}
|
||||
} else if (socket_name) {
|
||||
*out_transport = nsigner_transport_open_unix(socket_name, timeout_ms);
|
||||
if (!*out_transport) {
|
||||
fprintf(stderr, "error: cannot open unix transport %s\n", socket_name);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
/* Auto-discover: enumerate abstract UNIX sockets */
|
||||
char names[64][64];
|
||||
int count = nsigner_transport_list_unix(names, 64);
|
||||
if (count == 0) {
|
||||
fprintf(stderr, "error: no n_signer sockets found. Is n_signer running?\n");
|
||||
return -1;
|
||||
}
|
||||
if (count > 1) {
|
||||
fprintf(stderr, "error: multiple n_signer sockets found. Use --socket-name to select one:\n");
|
||||
for (int j = 0; j < count; j++) {
|
||||
fprintf(stderr, " %s\n", names[j]);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
*out_transport = nsigner_transport_open_unix(names[0], timeout_ms);
|
||||
if (!*out_transport) {
|
||||
fprintf(stderr, "error: cannot open unix transport %s\n", names[0]);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Main */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
/* ---- globals ---- */
|
||||
const char *socket_name = NULL;
|
||||
int timeout_ms = 5000;
|
||||
const char *tcp_arg = NULL;
|
||||
const char *serial_arg = NULL;
|
||||
const char *qrexec_arg = NULL;
|
||||
const char *auth_privkey_hex = NULL;
|
||||
const char *auth_label = NULL;
|
||||
|
||||
/* ---- selectors (nostr verbs) ---- */
|
||||
const char *role = NULL;
|
||||
const char *path = NULL;
|
||||
int has_index = 0;
|
||||
int index_val = 0;
|
||||
|
||||
/* ---- algorithm options ---- */
|
||||
const char *algorithm = NULL;
|
||||
int alg_index = 0;
|
||||
int has_alg_index = 0;
|
||||
const char *scheme = NULL;
|
||||
const char *encoding = NULL;
|
||||
const char *format = NULL;
|
||||
|
||||
/* ---- mine-event options ---- */
|
||||
int has_difficulty = 0;
|
||||
int difficulty_val = 0;
|
||||
int has_threads = 0;
|
||||
int threads_val = 1;
|
||||
int has_timeout_sec = 0;
|
||||
int timeout_sec_val = 0;
|
||||
|
||||
const char *prog = argv[0];
|
||||
|
||||
/* ---- parse global options ---- */
|
||||
int i = 1;
|
||||
while (i < argc && argv[i][0] == '-') {
|
||||
const char *arg = argv[i];
|
||||
|
||||
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||
print_usage(stderr, prog);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (strcmp(arg, "--socket-name") == 0 || strcmp(arg, "-n") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --socket-name requires an argument\n"); return 2; }
|
||||
socket_name = argv[++i];
|
||||
} else if (strcmp(arg, "--timeout") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --timeout requires an argument\n"); return 2; }
|
||||
timeout_ms = atoi(argv[++i]);
|
||||
if (timeout_ms <= 0) { fprintf(stderr, "error: --timeout must be positive\n"); return 2; }
|
||||
} else if (strcmp(arg, "--tcp") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --tcp requires <host:port>\n"); return 2; }
|
||||
tcp_arg = argv[++i];
|
||||
} else if (strcmp(arg, "--serial") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --serial requires a device path\n"); return 2; }
|
||||
serial_arg = argv[++i];
|
||||
} else if (strcmp(arg, "--qrexec") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --qrexec requires <qube:service>\n"); return 2; }
|
||||
qrexec_arg = argv[++i];
|
||||
} else if (strcmp(arg, "--auth-privkey") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --auth-privkey requires a 32-byte hex key\n"); return 2; }
|
||||
auth_privkey_hex = argv[++i];
|
||||
} else if (strcmp(arg, "--auth-label") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --auth-label requires a label\n"); return 2; }
|
||||
auth_label = argv[++i];
|
||||
} else if (strcmp(arg, "--role") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --role requires a name\n"); return 2; }
|
||||
role = argv[++i];
|
||||
} else if (strcmp(arg, "--path") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --path requires a BIP-44 derivation path\n"); return 2; }
|
||||
path = argv[++i];
|
||||
} else if (strcmp(arg, "--index") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --index requires a number\n"); return 2; }
|
||||
has_index = 1;
|
||||
index_val = atoi(argv[++i]);
|
||||
} else if (strcmp(arg, "--algorithm") == 0 || strcmp(arg, "-a") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --algorithm requires a name\n"); return 2; }
|
||||
algorithm = argv[++i];
|
||||
} else if (strcmp(arg, "--scheme") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --scheme requires schnorr or edsa\n"); return 2; }
|
||||
scheme = argv[++i];
|
||||
} else if (strcmp(arg, "--encoding") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --encoding requires ascii or binary\n"); return 2; }
|
||||
encoding = argv[++i];
|
||||
} else if (strcmp(arg, "--format") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --format requires plain or structured\n"); return 2; }
|
||||
format = argv[++i];
|
||||
} else if (strcmp(arg, "--difficulty") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --difficulty requires a number\n"); return 2; }
|
||||
has_difficulty = 1;
|
||||
difficulty_val = atoi(argv[++i]);
|
||||
} else if (strcmp(arg, "--threads") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --threads requires a number\n"); return 2; }
|
||||
has_threads = 1;
|
||||
threads_val = atoi(argv[++i]);
|
||||
} else if (strcmp(arg, "--timeout-sec") == 0) {
|
||||
if (i + 1 >= argc) { fprintf(stderr, "error: --timeout-sec requires a number\n"); return 2; }
|
||||
has_timeout_sec = 1;
|
||||
timeout_sec_val = atoi(argv[++i]);
|
||||
} else {
|
||||
fprintf(stderr, "error: unknown option: %s\n", arg);
|
||||
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
|
||||
return 2;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
/* ---- verb ---- */
|
||||
if (i >= argc) {
|
||||
fprintf(stderr, "error: no verb specified\n");
|
||||
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
|
||||
return 2;
|
||||
}
|
||||
const char *verb = argv[i++];
|
||||
|
||||
/* ---- verb args ---- */
|
||||
const char *arg1 = (i < argc) ? argv[i++] : NULL;
|
||||
const char *arg2 = (i < argc) ? argv[i++] : NULL;
|
||||
|
||||
/* ---- validate --index usage (algorithm-only now) ---- */
|
||||
if (has_index && !algorithm) {
|
||||
fprintf(stderr, "error: --index is only valid with --algorithm (for algorithm verbs)\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* ---- determine if this is an algorithm verb ---- */
|
||||
int is_algorithm_verb = (algorithm != NULL);
|
||||
|
||||
/* ---- validate --role and --path for nostr verbs ---- */
|
||||
int is_nostr_verb = (strcmp(verb, "get-public-key") == 0 ||
|
||||
strcmp(verb, "sign-event") == 0 ||
|
||||
strcmp(verb, "mine-event") == 0 ||
|
||||
strcmp(verb, "nip04-encrypt") == 0 ||
|
||||
strcmp(verb, "nip04-decrypt") == 0 ||
|
||||
strcmp(verb, "nip44-encrypt") == 0 ||
|
||||
strcmp(verb, "nip44-decrypt") == 0);
|
||||
if (is_nostr_verb && !is_algorithm_verb) {
|
||||
if (!role) {
|
||||
fprintf(stderr, "error: --role is required for nostr verbs\n");
|
||||
return 2;
|
||||
}
|
||||
if (!path) {
|
||||
fprintf(stderr, "error: --path is required for nostr verbs\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- nostr_init ---- */
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: failed to initialize crypto subsystem\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* ---- list verb (no connection needed) ---- */
|
||||
if (strcmp(verb, "list") == 0) {
|
||||
char names[64][64];
|
||||
int count = nsigner_transport_list_unix(names, 64);
|
||||
if (count == 0) {
|
||||
printf("no n_signer sockets found\n");
|
||||
} else {
|
||||
for (int j = 0; j < count; j++) {
|
||||
printf("%s\n", names[j]);
|
||||
}
|
||||
}
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- open transport ---- */
|
||||
nsigner_transport_t *transport = NULL;
|
||||
if (open_transport(socket_name, timeout_ms, tcp_arg, serial_arg, qrexec_arg,
|
||||
auth_privkey_hex, &transport) != 0) {
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* ---- create low-level client (owns transport) ---- */
|
||||
nsigner_client_t *client = nsigner_client_new(transport);
|
||||
if (!client) {
|
||||
fprintf(stderr, "error: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
transport = NULL; /* owned by client */
|
||||
|
||||
/* ---- create high-level signer from client (shares the connection) ---- */
|
||||
nostr_signer_t *signer = nostr_signer_nsigner_from_client(client, role);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "error: cannot create nsigner signer\n");
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
/* signer now owns client; don't free it separately */
|
||||
|
||||
/* ---- set role_path selector for nostr verbs ---- */
|
||||
if (is_nostr_verb && !is_algorithm_verb && path) {
|
||||
if (nostr_signer_nsigner_set_role_path(signer, path) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: failed to set role_path\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- auth envelope (TCP) ---- */
|
||||
if (auth_privkey_hex) {
|
||||
unsigned char privkey[32];
|
||||
if (hex_to_bytes(auth_privkey_hex, privkey, 32) != 32) {
|
||||
fprintf(stderr, "error: --auth-privkey must be 32 bytes (64 hex chars)\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
if (nostr_signer_nsigner_set_auth(signer, privkey, auth_label ? auth_label : "") != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: failed to set auth envelope\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- resolve algorithm index ---- */
|
||||
int eff_index = has_alg_index ? alg_index : (has_index ? index_val : 0);
|
||||
|
||||
int rc = 2;
|
||||
char *result_str = NULL;
|
||||
cJSON *result_obj = NULL;
|
||||
|
||||
/* ---- dispatch verbs via high-level library wrappers ---- */
|
||||
if (strcmp(verb, "get-info") == 0) {
|
||||
rc = nostr_signer_get_info(signer, &result_obj);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
|
||||
goto cleanup;
|
||||
}
|
||||
char *json = cJSON_PrintUnformatted(result_obj);
|
||||
if (json) { printf("%s\n", json); free(json); }
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "get-public-key") == 0) {
|
||||
if (is_algorithm_verb) {
|
||||
rc = nostr_signer_get_public_key_alg(signer, algorithm, eff_index, &result_str);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
|
||||
goto cleanup;
|
||||
}
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (format && strcmp(format, "structured") == 0) {
|
||||
/* Structured format: use low-level client to pass the format option. */
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
cJSON *opts = cJSON_CreateObject();
|
||||
if (role) cJSON_AddStringToObject(opts, "role", role);
|
||||
if (path) cJSON_AddStringToObject(opts, "role_path", path);
|
||||
cJSON_AddStringToObject(opts, "format", "structured");
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
cJSON *presult = NULL;
|
||||
rc = nsigner_client_call(client, "nostr_get_public_key", params, &presult);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nsigner_client_last_error(client)); goto cleanup; }
|
||||
if (cJSON_IsString(presult)) {
|
||||
print_result_str(presult->valuestring);
|
||||
} else if (presult) {
|
||||
char *json = cJSON_PrintUnformatted(presult);
|
||||
if (json) { printf("%s\n", json); free(json); }
|
||||
}
|
||||
cJSON_Delete(presult);
|
||||
rc = 0;
|
||||
} else {
|
||||
char pubkey_hex[65];
|
||||
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("%s\n", pubkey_hex);
|
||||
rc = 0;
|
||||
}
|
||||
} else if (strcmp(verb, "sign-event") == 0) {
|
||||
const char *event_json = arg1;
|
||||
char *event_buf = NULL;
|
||||
if (!event_json) {
|
||||
event_buf = read_stdin_line();
|
||||
if (!event_buf) {
|
||||
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
event_json = event_buf;
|
||||
}
|
||||
cJSON *event = cJSON_Parse(event_json);
|
||||
free(event_buf);
|
||||
if (!event) {
|
||||
fprintf(stderr, "error: failed to parse event JSON\n");
|
||||
goto cleanup;
|
||||
}
|
||||
rc = nostr_signer_sign_event(signer, event, &result_obj);
|
||||
cJSON_Delete(event);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
|
||||
goto cleanup;
|
||||
}
|
||||
char *json = cJSON_PrintUnformatted(result_obj);
|
||||
if (json) { printf("%s\n", json); free(json); }
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "mine-event") == 0) {
|
||||
const char *event_json = arg1;
|
||||
char *event_buf = NULL;
|
||||
if (!event_json) {
|
||||
event_buf = read_stdin_line();
|
||||
if (!event_buf) {
|
||||
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
event_json = event_buf;
|
||||
}
|
||||
cJSON *event = cJSON_Parse(event_json);
|
||||
free(event_buf);
|
||||
if (!event) {
|
||||
fprintf(stderr, "error: failed to parse event JSON\n");
|
||||
goto cleanup;
|
||||
}
|
||||
rc = nostr_signer_mine_event(signer, event,
|
||||
has_difficulty ? difficulty_val : 0,
|
||||
has_timeout_sec ? timeout_sec_val : 0,
|
||||
has_threads ? threads_val : 1,
|
||||
&result_obj);
|
||||
cJSON_Delete(event);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
|
||||
goto cleanup;
|
||||
}
|
||||
char *json = cJSON_PrintUnformatted(result_obj);
|
||||
if (json) { printf("%s\n", json); free(json); }
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "nip04-encrypt") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: nip04-encrypt requires <peer-pubkey>\n"); goto cleanup; }
|
||||
const char *plaintext = arg2;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
rc = nostr_signer_nip04_encrypt(signer, arg1, plaintext, &result_str);
|
||||
free(pt_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "nip04-decrypt") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: nip04-decrypt requires <peer-pubkey>\n"); goto cleanup; }
|
||||
const char *ciphertext = arg2;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
rc = nostr_signer_nip04_decrypt(signer, arg1, ciphertext, &result_str);
|
||||
free(ct_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "nip44-encrypt") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: nip44-encrypt requires <peer-pubkey>\n"); goto cleanup; }
|
||||
const char *plaintext = arg2;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
rc = nostr_signer_nip44_encrypt(signer, arg1, plaintext, &result_str);
|
||||
free(pt_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "nip44-decrypt") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: nip44-decrypt requires <peer-pubkey>\n"); goto cleanup; }
|
||||
const char *ciphertext = arg2;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
rc = nostr_signer_nip44_decrypt(signer, arg1, ciphertext, &result_str);
|
||||
free(ct_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "sign") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: sign requires <msg-hex>\n"); goto cleanup; }
|
||||
size_t msg_len = strlen(arg1) / 2;
|
||||
unsigned char *msg = malloc(msg_len ? msg_len : 1);
|
||||
if (!msg) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
|
||||
int n = hex_to_bytes(arg1, msg, msg_len);
|
||||
if (n < 0) { free(msg); fprintf(stderr, "error: invalid hex message\n"); goto cleanup; }
|
||||
rc = nostr_signer_sign(signer, algorithm ? algorithm : "secp256k1",
|
||||
eff_index, scheme, msg, (size_t)n, &result_str);
|
||||
free(msg);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "verify") == 0) {
|
||||
if (!arg1 || !arg2) { fprintf(stderr, "error: verify requires <msg-hex> <sig-hex>\n"); goto cleanup; }
|
||||
size_t msg_len = strlen(arg1) / 2;
|
||||
size_t sig_len = strlen(arg2) / 2;
|
||||
unsigned char *msg = malloc(msg_len ? msg_len : 1);
|
||||
unsigned char *sig = malloc(sig_len ? sig_len : 1);
|
||||
if (!msg || !sig) { free(msg); free(sig); fprintf(stderr, "error: out of memory\n"); goto cleanup; }
|
||||
int mn = hex_to_bytes(arg1, msg, msg_len);
|
||||
int sn = hex_to_bytes(arg2, sig, sig_len);
|
||||
if (mn < 0 || sn < 0) { free(msg); free(sig); fprintf(stderr, "error: invalid hex\n"); goto cleanup; }
|
||||
int valid = 0;
|
||||
rc = nostr_signer_verify(signer, algorithm ? algorithm : "secp256k1",
|
||||
eff_index, scheme, msg, (size_t)mn, sig, (size_t)sn, &valid);
|
||||
free(msg);
|
||||
free(sig);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
printf("%s\n", valid ? "valid" : "invalid");
|
||||
rc = valid ? 0 : 1;
|
||||
} else if (strcmp(verb, "derive") == 0) {
|
||||
const char *data = arg1;
|
||||
char *data_buf = NULL;
|
||||
if (!data) {
|
||||
data_buf = read_stdin_line();
|
||||
if (!data_buf) { fprintf(stderr, "error: no data provided\n"); goto cleanup; }
|
||||
data = data_buf;
|
||||
}
|
||||
if (is_algorithm_verb) {
|
||||
/* Algorithm-based derive: use low-level client to pass algorithm+index. */
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(data));
|
||||
cJSON *opts = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "secp256k1");
|
||||
cJSON_AddNumberToObject(opts, "index", eff_index);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
cJSON *dresult = NULL;
|
||||
rc = nsigner_client_call(client, "derive", params, &dresult);
|
||||
free(data_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nsigner_client_last_error(client)); goto cleanup; }
|
||||
if (cJSON_IsString(dresult)) {
|
||||
/* The derive result is a JSON object string like
|
||||
* {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"}.
|
||||
* Print the raw result string. */
|
||||
print_result_str(dresult->valuestring);
|
||||
}
|
||||
cJSON_Delete(dresult);
|
||||
rc = 0;
|
||||
} else {
|
||||
/* Nostr derive (HMAC): use the high-level wrapper. */
|
||||
char digest_hex[65];
|
||||
rc = nostr_signer_derive_hmac(signer, data, digest_hex);
|
||||
free(data_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
printf("%s\n", digest_hex);
|
||||
rc = 0;
|
||||
}
|
||||
} else if (strcmp(verb, "encapsulate") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: encapsulate requires <peer-pubkey-hex>\n"); goto cleanup; }
|
||||
rc = nostr_signer_encapsulate(signer, arg1, &result_str);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "decapsulate") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: decapsulate requires <ciphertext-hex>\n"); goto cleanup; }
|
||||
rc = nostr_signer_decapsulate(signer, eff_index, arg1, &result_str);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "derive-shared-secret") == 0) {
|
||||
if (!arg1) { fprintf(stderr, "error: derive-shared-secret requires <peer-pubkey-hex>\n"); goto cleanup; }
|
||||
rc = nostr_signer_derive_shared_secret(signer, eff_index, arg1, &result_str);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "encrypt") == 0) {
|
||||
const char *plaintext = arg1;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
rc = nostr_signer_otp_encrypt(signer, plaintext, encoding, &result_str);
|
||||
free(pt_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "decrypt") == 0) {
|
||||
const char *ciphertext = arg1;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
rc = nostr_signer_otp_decrypt(signer, ciphertext, encoding, &result_str);
|
||||
free(ct_buf);
|
||||
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
|
||||
print_result_str(result_str);
|
||||
rc = 0;
|
||||
} else if (strcmp(verb, "call") == 0) {
|
||||
/* Raw passthrough using the low-level client (shared with signer). */
|
||||
if (!arg1) { fprintf(stderr, "error: call requires <method>\n"); goto cleanup; }
|
||||
const char *method = arg1;
|
||||
|
||||
cJSON *params = NULL;
|
||||
if (arg2) {
|
||||
size_t total = 0;
|
||||
for (int j = i - 1; j < argc; j++) {
|
||||
total += strlen(argv[j]) + 1;
|
||||
}
|
||||
char *json_str = malloc(total + 1);
|
||||
if (!json_str) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
|
||||
json_str[0] = '\0';
|
||||
for (int j = i - 1; j < argc; j++) {
|
||||
strcat(json_str, argv[j]);
|
||||
if (j + 1 < argc) strcat(json_str, " ");
|
||||
}
|
||||
params = cJSON_Parse(json_str);
|
||||
free(json_str);
|
||||
if (!params) {
|
||||
fprintf(stderr, "error: failed to parse params JSON from argv\n");
|
||||
goto cleanup;
|
||||
}
|
||||
} else {
|
||||
char *line = read_stdin_line();
|
||||
if (!line) {
|
||||
fprintf(stderr, "error: no params JSON on stdin\n");
|
||||
goto cleanup;
|
||||
}
|
||||
params = cJSON_Parse(line);
|
||||
free(line);
|
||||
if (!params) {
|
||||
fprintf(stderr, "error: failed to parse params JSON from stdin\n");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *call_result = NULL;
|
||||
if (nsigner_client_call(client, method, params, &call_result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
if (call_result) {
|
||||
char *json = cJSON_PrintUnformatted(call_result);
|
||||
if (json) { printf("%s\n", json); free(json); }
|
||||
cJSON_Delete(call_result);
|
||||
}
|
||||
rc = 0;
|
||||
} else {
|
||||
fprintf(stderr, "error: unknown verb: %s\n", verb);
|
||||
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (result_str) free(result_str);
|
||||
if (result_obj) cJSON_Delete(result_obj);
|
||||
if (signer) nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
# Plan: `n_signer_client` — Linux CLI for n_signer
|
||||
|
||||
## Goal
|
||||
|
||||
A standalone Linux command-line client `n_signer_client` that connects to a
|
||||
running `n_signer` process over its abstract UNIX socket (and optionally the
|
||||
other framed transports) and exposes the full verb surface over stdin/stdout so
|
||||
that signed events can be piped directly into `nak publish`.
|
||||
|
||||
## Deliverable & placement
|
||||
|
||||
The project lives in [`client/`](.) alongside the existing demo clients
|
||||
(`demo_c99.c`, `demo_javascript.js`, `demo_python.py`):
|
||||
|
||||
- New file: [`client/n_signer_client.c`](n_signer_client.c) — single-file C99 program.
|
||||
- New file: [`client/n_signer_client_README.md`](n_signer_client_README.md) — dedicated README just for this client (usage, verbs, pipe-to-nak recipes, build instructions). The existing [`client/README.md`](README.md) stays as-is (it documents the nostr_core_lib migration).
|
||||
- New Makefile target producing `build/n_signer_client`.
|
||||
|
||||
The binary links `nostr_core_lib` exactly like the existing examples
|
||||
[`examples/sign_event_client.c`](../examples/sign_event_client.c) and
|
||||
[`examples/get_public_key_client.c`](../examples/get_public_key_client.c). It
|
||||
uses:
|
||||
|
||||
- `nsigner_transport_open_unix` (and optionally `_tcp`, `_serial`, `_qrexec`) from `nostr_core_lib/nostr_core/nsigner_transport.h`
|
||||
- `nsigner_client_new` / `nsigner_client_free` from `nostr_core_lib/nostr_core/nsigner_client.h`
|
||||
- `nsigner_client_call` (takes ownership of `params`)
|
||||
- `nsigner_client_set_auth` for TCP mode
|
||||
|
||||
## CLI shape
|
||||
|
||||
```
|
||||
n_signer_client [global options] <verb> [verb args...]
|
||||
```
|
||||
|
||||
Global options:
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `--socket-name`, `-n <name>` | auto-discover | Abstract socket name without `@` |
|
||||
| `--timeout <ms>` | `5000` | Transport timeout |
|
||||
| `--tcp <host:port>` | none | Use TCP transport (requires `--auth-privkey`) |
|
||||
| `--serial <device>` | none | Use USB CDC-ACM serial transport |
|
||||
| `--qrexec <qube:service>` | none | Use Qubes qrexec transport |
|
||||
| `--auth-privkey <32-byte hex>` | none | Auth envelope privkey for TCP |
|
||||
| `--auth-label <text>` | none | Auth envelope label |
|
||||
|
||||
Selector options (apply to `nostr_*` verbs; `--role` and `--path` are mutually exclusive):
|
||||
|
||||
| Flag | Meaning | JSON emitted |
|
||||
|---|---|---|
|
||||
| `--role <name>` | Named path-role registered in the signer's wizard | `{"role":"<name>"}` |
|
||||
| `--path <full-path>` | Full BIP-44 derivation path | `{"role_path":"<full-path>"}` |
|
||||
| `--index <N>` | Optional variable-segment index for a named path-role (only valid with `--role`) | adds `"index":N` to the role object |
|
||||
|
||||
Algorithm options (apply to algorithm-based verbs):
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `--algorithm <alg>` | none | `secp256k1`/`ed25519`/`x25519`/`ml-dsa-65`/`slh-dsa-128s`/`ml-kem-768`/`otp` |
|
||||
| `--index <N>` | `0` | Algorithm derivation index (substituted into the alg's path) |
|
||||
| `--scheme <schnorr\|ecdsa>` | `schnorr` | secp256k1 `sign`/`verify` only |
|
||||
| `--encoding <base64\|hex>` | `base64` | OTP `encrypt`/`decrypt` only |
|
||||
| `--format <plain\|structured>` | `plain` | `nostr_get_public_key` output shape |
|
||||
|
||||
Note on `--index` overload: when `--algorithm` is set, `--index` is the
|
||||
algorithm derivation index. When `--role` is set (and no `--algorithm`),
|
||||
`--index` is the named path-role's variable-segment index. These two contexts
|
||||
never overlap because algorithm verbs and `nostr_*` verbs are distinct.
|
||||
|
||||
Auto-discovery: when no `--socket-name` and no explicit transport is given,
|
||||
enumerate via `nsigner_transport_list_unix` and proceed only if exactly one
|
||||
`nsigner*` socket exists (mirror `discover_single_socket_name` in
|
||||
[`src/main.c`](../src/main.c)).
|
||||
|
||||
## Verb surface (full)
|
||||
|
||||
Per [`README.md`](../README.md) §4.3 verb table. The options object is always
|
||||
the trailing element of the `params` array.
|
||||
|
||||
### Metadata
|
||||
|
||||
| Verb | RPC method | stdout |
|
||||
|---|---|---|
|
||||
| `get-info` | `get_info` | raw `result` JSON (name, version, verbs, algorithms) |
|
||||
|
||||
### Nostr verbs (role-based; selector from `--role` / `--path`)
|
||||
|
||||
| Verb | RPC method | stdin/argv | stdout |
|
||||
|---|---|---|---|
|
||||
| `get-public-key` | `nostr_get_public_key` | none | pubkey hex (or structured JSON with `--format structured`) |
|
||||
| `sign-event` | `nostr_sign_event` | event JSON from argv or one stdin line | signed event JSON, one line |
|
||||
| `mine-event` | `nostr_mine_event` | event JSON from argv or stdin; options `--difficulty`, `--threads`, `--timeout-sec` | signed mined event JSON |
|
||||
| `nip04-encrypt <peer-pubkey>` | `nostr_nip04_encrypt` | plaintext from argv or stdin | ciphertext |
|
||||
| `nip04-decrypt <peer-pubkey>` | `nostr_nip04_decrypt` | ciphertext from argv or stdin | plaintext |
|
||||
| `nip44-encrypt <peer-pubkey>` | `nostr_nip44_encrypt` | plaintext from argv or stdin | ciphertext |
|
||||
| `nip44-decrypt <peer-pubkey>` | `nostr_nip44_decrypt` | ciphertext from argv or stdin | plaintext |
|
||||
|
||||
### Algorithm-based verbs (use `--algorithm` and `--index`)
|
||||
|
||||
| Verb | RPC method | argv | stdout |
|
||||
|---|---|---|---|
|
||||
| `get-public-key` | `get_public_key` | none | structured JSON `{"algorithm":...,"public_key":...,"key_id":...}` |
|
||||
| `sign <msg-hex>` | `sign` | hex bytes | structured JSON `{"signature":...,"algorithm":...,"key_id":...}` |
|
||||
| `verify <msg-hex> <sig-hex>` | `verify` | hex bytes | `valid` / `invalid` (exit 0/1) |
|
||||
| `derive <data>` | `derive` | UTF-8 data (argv or stdin) | structured JSON `{"algorithm":...,"key_id":...,"digest":...}` |
|
||||
| `encapsulate <peer-pubkey-hex>` | `encapsulate` | hex | structured JSON `{"ciphertext":...,"shared_secret":...}` |
|
||||
| `decapsulate <ciphertext-hex>` | `decapsulate` | hex | structured JSON `{"shared_secret":...}` |
|
||||
| `derive-shared-secret <peer-pubkey-hex>` | `derive_shared_secret` | hex | shared secret hex |
|
||||
| `encrypt <plaintext>` | `encrypt` | plaintext (base64 by default; `--encoding hex`) | ciphertext |
|
||||
| `decrypt <ciphertext>` | `decrypt` | ciphertext | plaintext |
|
||||
|
||||
### Generic escape hatch
|
||||
|
||||
| Verb | RPC method | input | stdout |
|
||||
|---|---|---|---|
|
||||
| `call <method>` | `<method>` | JSON `params` array from stdin (one line) or argv | raw `result` JSON |
|
||||
|
||||
This keeps the client future-proof for any new server verb without a CLI rewrite.
|
||||
|
||||
## stdin/stdout contract (pipe-friendly)
|
||||
|
||||
- All payload output goes to stdout as a single line, newline-terminated.
|
||||
- All diagnostics go to stderr.
|
||||
- Exit code: `0` on success, non-zero on transport/RPC error (use
|
||||
`nsigner_client_last_error` for the message). For `verify`, exit `0` =
|
||||
valid, `1` = invalid, `2` = error.
|
||||
- `sign-event` reads event JSON from argv if present, else reads exactly one
|
||||
line from stdin. This is the pipe-to-nak path:
|
||||
|
||||
```bash
|
||||
echo '{"kind":1,"content":"hello","tags":[],"created_at":1700000000}' \
|
||||
| n_signer_client --role main sign-event \
|
||||
| nak publish
|
||||
```
|
||||
|
||||
- `nip04-encrypt` / `nip44-encrypt` read plaintext from argv or stdin.
|
||||
- `nip04-decrypt` / `nip44-decrypt` read ciphertext from argv or stdin.
|
||||
- `sign` / `verify` / `encapsulate` / `decapsulate` / `derive-shared-secret`
|
||||
take hex from argv (binary payloads, not pipe-friendly text).
|
||||
- `derive` takes UTF-8 data from argv or stdin.
|
||||
- `encrypt` / `decrypt` take their payload from argv or stdin (base64 by
|
||||
default per the server contract).
|
||||
- `call` reads a JSON `params` array from stdin (one line) or argv.
|
||||
|
||||
## Selector handling (per README §4.6)
|
||||
|
||||
The `nostr_*` verbs select a secp256k1 NIP-06 key via the options object. The
|
||||
client builds the options object from the selector flags:
|
||||
|
||||
- `--role <name>` → `{"role":"<name>"}` (named path-role; the derivation path
|
||||
is hidden from the client by the signer).
|
||||
- `--role <name> --index <N>` → `{"role":"<name>","index":N}` (named path-role
|
||||
with variable-segment index; rejected with `2005 index_out_of_range` if out
|
||||
of the role's range).
|
||||
- `--path <full-path>` → `{"role_path":"<full-path>"}` (raw BIP-44 path; must
|
||||
match a registered role's path template or be explicitly allowed).
|
||||
- Default (no selector): server uses the default role `main`.
|
||||
- Conflicting selectors → client-side error (do not send; the server would
|
||||
reject with `ambiguous_role_selector` 1001).
|
||||
|
||||
Resolution order on the server: `role` → `role_path` → default `main`. The
|
||||
client enforces mutual exclusivity of the selector flags before sending.
|
||||
|
||||
For algorithm verbs, `--algorithm` and `--index` populate the options object
|
||||
instead; `--scheme` adds `"scheme"` for secp256k1 sign/verify; `--encoding`
|
||||
adds `"encoding"` for OTP encrypt/decrypt.
|
||||
|
||||
## Transport
|
||||
|
||||
- Default: UNIX abstract socket via `nsigner_transport_open_unix(name, timeout_ms)`.
|
||||
- `--tcp host:port` → `nsigner_transport_open_tcp` (requires `--auth-privkey`
|
||||
32-byte hex; calls `nsigner_client_set_auth` with `--auth-label`).
|
||||
- `--serial /dev/ttyACM0` → `nsigner_transport_open_serial`.
|
||||
- `--qrexec qube:service` → `nsigner_transport_open_qrexec`.
|
||||
- The vtable is uniform so all four transports share the same call path after
|
||||
construction.
|
||||
|
||||
## Build
|
||||
|
||||
Add to [`Makefile`](../Makefile):
|
||||
|
||||
```make
|
||||
N_SIGNER_CLIENT_TARGET := $(BUILD_DIR)/n_signer_client
|
||||
|
||||
clients: $(N_SIGNER_CLIENT_TARGET)
|
||||
|
||||
$(N_SIGNER_CLIENT_TARGET): $(CLIENT_DIR)/n_signer_client.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(CLIENT_DIR)/n_signer_client.c -o $(N_SIGNER_CLIENT_TARGET) $(LDFLAGS)
|
||||
```
|
||||
|
||||
Add `clients` to the `all` aggregate and to the `test-client` target so it is
|
||||
built alongside the examples.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Manual smoke test against a running `nsigner`:
|
||||
- `n_signer_client get-info` → signer metadata JSON.
|
||||
- `n_signer_client --role main get-public-key` → 64-hex pubkey.
|
||||
- `echo '{"kind":1,"content":"hello","tags":[],"created_at":1}' | n_signer_client --role main sign-event` → signed event with `id`, `pubkey`, `sig`.
|
||||
- Pipe to `nak event` / `nak publish` to verify the signed event is well-formed.
|
||||
- `n_signer_client --algorithm ed25519 --index 0 sign 68656c6c6f` → structured sig JSON.
|
||||
- `n_signer_client --role myrole get-public-key` → pubkey for the named path-role.
|
||||
2. Optional bash script `tests/test_n_signer_client.sh` that:
|
||||
- Spawns `nsigner --socket-name nsigner_test --listen unix --mnemonic-stdin` with a fixed test mnemonic.
|
||||
- Runs each verb and asserts on stdout shape.
|
||||
- Tears down the server.
|
||||
|
||||
## Mermaid flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[stdin or argv event JSON] --> B[n_signer_client sign-event]
|
||||
B --> C[nsigner_transport_open_unix]
|
||||
C --> D[nsigner_client_call nostr_sign_event]
|
||||
D --> E[nsigner @nsigner socket]
|
||||
E --> F[signed event JSON result]
|
||||
F --> G[stdout one line]
|
||||
G --> H[nak publish]
|
||||
```
|
||||
|
||||
## Out of scope
|
||||
|
||||
- No TUI, no approval UI — the human attendant lives in the running `nsigner`
|
||||
process; the client is just a thin wire caller.
|
||||
- No key storage, no mnemonic handling.
|
||||
- No HTTP listener client (the `http_listener` is server-side; the client uses
|
||||
the framed transports).
|
||||
- No NIP-46 bunker mode (covered separately by
|
||||
[`plans/nip46_bunker_mode.md`](../plans/nip46_bunker_mode.md)).
|
||||
@@ -0,0 +1,171 @@
|
||||
# `nsigner_client` — Linux CLI for n_signer
|
||||
|
||||
A standalone Linux command-line client that connects to a running [`n_signer`](https://github.com/your-org/n_signer) process and calls its JSON-RPC verbs over stdin/stdout. Designed for pipe-to-`nak` workflows.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make clients
|
||||
```
|
||||
|
||||
Produces `build/nsigner_client`. Links `nostr_core_lib` exactly like the existing examples.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
nsigner_client [global options] <verb> [verb args...]
|
||||
```
|
||||
|
||||
### Global options
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
|------|---------|---------|
|
||||
| `--socket-name`, `-n <name>` | auto-discover | Abstract socket name without `@` |
|
||||
| `--timeout <ms>` | `5000` | Transport timeout |
|
||||
| `--tcp <host:port>` | none | TCP transport (requires `--auth-privkey`) |
|
||||
| `--serial <device>` | none | USB CDC-ACM serial transport |
|
||||
| `--qrexec <qube:service>` | none | Qubes qrexec transport |
|
||||
| `--auth-privkey <32-byte hex>` | none | Auth envelope privkey for TCP |
|
||||
| `--auth-label <text>` | none | Auth envelope label |
|
||||
|
||||
### Selector options (for `nostr_*` verbs)
|
||||
|
||||
| Flag | Meaning | JSON emitted |
|
||||
|------|---------|-------------|
|
||||
| `--role <name>` | Named path-role registered in the signer | `{"role":"<name>"}` |
|
||||
| `--path <path>` | Full BIP-44 derivation path | `{"role_path":"<path>"}` |
|
||||
|
||||
### Algorithm options (for algorithm-based verbs)
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
|------|---------|---------|
|
||||
| `--algorithm <alg>` | none | `secp256k1`/`ed25519`/`x25519`/`ml-dsa-65`/`slh-dsa-128s`/`ml-kem-768`/`otp` |
|
||||
| `--index <N>` | `0` | Algorithm derivation index |
|
||||
| `--scheme <schnork\|ecdsa>` | `schnorr` | secp256k1 `sign`/`verify` only |
|
||||
| `--encoding <base64\|hex>` | `base64` | OTP `encrypt`/`decrypt` only |
|
||||
| `--format <plain\|structured>` | `plain` | `nostr_get_public_key` output shape |
|
||||
|
||||
### Mine-event options
|
||||
|
||||
| Flag | Meaning |
|
||||
|------|---------|
|
||||
| `--difficulty <N>` | Target leading zero bits |
|
||||
| `--threads <N>` | Mining threads (default 1) |
|
||||
| `--timeout-sec <N>` | Mining timeout in seconds |
|
||||
|
||||
## Verb reference
|
||||
|
||||
### Utility
|
||||
|
||||
| Verb | stdout |
|
||||
|------|--------|
|
||||
| `list` | Lists running n_signer abstract sockets (one `@name` per line) |
|
||||
|
||||
### Metadata
|
||||
|
||||
| Verb | RPC method | stdout |
|
||||
|------|------------|--------|
|
||||
| `get-info` | `get_info` | raw result JSON (name, version, verbs, algorithms) |
|
||||
|
||||
### Nostr verbs (role-based)
|
||||
|
||||
| Verb | RPC method | stdin/argv | stdout |
|
||||
|------|------------|------------|--------|
|
||||
| `get-public-key` | `nostr_get_public_key` | none | pubkey hex (or structured JSON with `--format structured`) |
|
||||
| `sign-event` | `nostr_sign_event` | event JSON from argv or stdin | signed event JSON |
|
||||
| `mine-event` | `nostr_mine_event` | event JSON from argv or stdin | signed mined event JSON |
|
||||
| `nip04-encrypt <peer>` | `nostr_nip04_encrypt` | plaintext from argv or stdin | ciphertext |
|
||||
| `nip04-decrypt <peer>` | `nostr_nip04_decrypt` | ciphertext from argv or stdin | plaintext |
|
||||
| `nip44-encrypt <peer>` | `nostr_nip44_encrypt` | plaintext from argv or stdin | ciphertext |
|
||||
| `nip44-decrypt <peer>` | `nostr_nip44_decrypt` | ciphertext from argv or stdin | plaintext |
|
||||
|
||||
### Algorithm-based verbs
|
||||
|
||||
| Verb | RPC method | argv | stdout |
|
||||
|------|------------|------|--------|
|
||||
| `get-public-key` | `get_public_key` | none | structured JSON `{"algorithm":...,"public_key":...,"key_id":...}` |
|
||||
| `sign <msg-hex>` | `sign` | hex bytes | structured JSON `{"signature":...,"algorithm":...,"key_id":...}` |
|
||||
| `verify <msg-hex> <sig-hex>` | `verify` | hex bytes | `valid` / `invalid` (exit 0/1) |
|
||||
| `derive <data>` | `derive` | UTF-8 data (argv or stdin) | structured JSON |
|
||||
| `encapsulate <peer-pubkey-hex>` | `encapsulate` | hex | structured JSON |
|
||||
| `decapsulate <ciphertext-hex>` | `decapsulate` | hex | structured JSON |
|
||||
| `derive-shared-secret <peer-pubkey-hex>` | `derive_shared_secret` | hex | shared secret hex |
|
||||
| `encrypt <plaintext>` | `encrypt` | plaintext (base64 by default) | ciphertext |
|
||||
| `decrypt <ciphertext>` | `decrypt` | ciphertext | plaintext |
|
||||
|
||||
### Generic escape hatch
|
||||
|
||||
| Verb | RPC method | input | stdout |
|
||||
|------|------------|-------|--------|
|
||||
| `call <method>` | `<method>` | JSON params array from stdin or argv | raw result JSON |
|
||||
|
||||
## Selector explanation
|
||||
|
||||
The `nostr_*` verbs select a key via the options object using both `--role` and `--path`:
|
||||
|
||||
- **`--role <name> --path <path>`** — Both are required for all `nostr_*` verbs. The role authorizes the request and determines the encryption scheme. The path selects the specific key to derive. Sends `{"role":"<name>","role_path":"<path>"}` to the server.
|
||||
- **`--role` without `--path`** — Client-side error: `--path is required for nostr verbs`.
|
||||
- **`--path` without `--role`** — Client-side error: `--role is required for nostr verbs`.
|
||||
|
||||
For algorithm verbs, `--algorithm` and `--index` populate the options object instead.
|
||||
|
||||
## Pipe-to-nak recipes
|
||||
|
||||
```bash
|
||||
# Get public key
|
||||
nsigner_client --role main --path "m/44'/1237'/0'/0/0" get-public-key
|
||||
|
||||
# Sign an event and publish via nak
|
||||
echo '{"kind":1,"content":"hello nostr","tags":[],"created_at":1700000000}' \
|
||||
| nsigner_client --role main --path "m/44'/1237'/0'/0/0" sign-event \
|
||||
| nak publish
|
||||
|
||||
# Mine a proof-of-work event
|
||||
echo '{"kind":1,"content":"pow","tags":[],"created_at":1700000000}' \
|
||||
| nsigner_client --role main --path "m/44'/1237'/0'/0/0" mine-event --difficulty 20 --threads 4
|
||||
|
||||
# NIP-44 encrypt
|
||||
nsigner_client --role main --path "m/44'/1237'/0'/0/0" nip44-encrypt <peer-pubkey> "secret message"
|
||||
|
||||
# Algorithm-based signing
|
||||
nsigner_client --algorithm ed25519 --index 0 sign 68656c6c6f
|
||||
|
||||
# Verify a signature
|
||||
nsigner_client --algorithm secp256k1 verify <msg-hex> <sig-hex> && echo "valid"
|
||||
|
||||
# Get signer info
|
||||
nsigner_client get-info
|
||||
```
|
||||
|
||||
## Transport options
|
||||
|
||||
| Transport | Flag | Notes |
|
||||
|-----------|------|-------|
|
||||
| UNIX abstract socket | `--socket-name <name>` or auto-discover | Default. Auto-discovers if exactly one `@nsigner*` socket exists. |
|
||||
| TCP | `--tcp <host:port>` | Requires `--auth-privkey` for auth envelope. |
|
||||
| Serial (USB CDC-ACM) | `--serial <device>` | e.g. `--serial /dev/ttyACM0` |
|
||||
| Qubes qrexec | `--qrexec <qube:service>` | e.g. `--qrexec sys-signer:qubes.NsignerRpc` |
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Invalid (verify verb only — signature is invalid) |
|
||||
| 2 | Error (transport, RPC, or usage error) |
|
||||
|
||||
For `verify`: exit 0 = valid signature, exit 1 = invalid signature, exit 2 = error.
|
||||
|
||||
## stdin/stdout contract
|
||||
|
||||
- All payload output goes to stdout as a single line, newline-terminated.
|
||||
- All diagnostics (errors, warnings) go to stderr.
|
||||
- `sign-event`, `nip04-*`, `nip44-*`, `derive`, `encrypt`, `decrypt` read their payload from argv if present, otherwise from stdin (one line).
|
||||
- `sign`, `verify`, `encapsulate`, `decapsulate`, `derive-shared-secret` take hex from argv only (binary payloads).
|
||||
- `call` reads a JSON params array from stdin (one line) or argv.
|
||||
|
||||
## See also
|
||||
|
||||
- [`n_signer_client_PLAN.md`](n_signer_client_PLAN.md) — the full implementation plan
|
||||
- [`README.md`](../README.md) — n_signer main documentation (API §4)
|
||||
- [`examples/sign_event_client.c`](../examples/sign_event_client.c) — reference example
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# deploy_local.sh — Build static nsigner + nsigner_client binaries
|
||||
# and install them to /usr/local/bin/
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy_local.sh # build + install (uses sudo if needed)
|
||||
# ./deploy_local.sh --no-build # install existing build/ binaries only
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
INSTALL_PREFIX="/usr/local/bin"
|
||||
|
||||
HOST_UNAME="$(uname -m)"
|
||||
case "$HOST_UNAME" in
|
||||
x86_64) ARCH="x86_64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
armv7l|armv7) ARCH="armv7" ;;
|
||||
*)
|
||||
echo "ERROR: Unsupported host architecture '$HOST_UNAME'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
SIGNER_BIN="$BUILD_DIR/nsigner_static_x86_64"
|
||||
CLIENT_BIN="$BUILD_DIR/nsigner_client_static_x86_64"
|
||||
;;
|
||||
arm64)
|
||||
SIGNER_BIN="$BUILD_DIR/nsigner_static_arm64"
|
||||
CLIENT_BIN="$BUILD_DIR/nsigner_client_static_arm64"
|
||||
;;
|
||||
armv7)
|
||||
SIGNER_BIN="$BUILD_DIR/nsigner_static_armv7"
|
||||
CLIENT_BIN="$BUILD_DIR/nsigner_client_static_armv7"
|
||||
;;
|
||||
esac
|
||||
|
||||
DO_BUILD=true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-build)
|
||||
DO_BUILD=false
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "deploy_local.sh — Build and install nsigner + nsigner_client to $INSTALL_PREFIX"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " --no-build Skip build step; install existing binaries from build/"
|
||||
echo " -h, --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument '$1'"
|
||||
echo "Usage: $0 [--no-build]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
echo "nsigner local deploy"
|
||||
echo "=========================================="
|
||||
echo "Architecture: $ARCH"
|
||||
echo "Install dir: $INSTALL_PREFIX"
|
||||
echo "Signer binary: $SIGNER_BIN"
|
||||
echo "Client binary: $CLIENT_BIN"
|
||||
echo ""
|
||||
|
||||
# --- Build step ---------------------------------------------------------------
|
||||
if $DO_BUILD; then
|
||||
echo "[1/3] Building static binaries via build_static.sh"
|
||||
echo ""
|
||||
bash "$SCRIPT_DIR/build_static.sh" --arch "$ARCH"
|
||||
echo ""
|
||||
else
|
||||
echo "[1/3] Skipping build (--no-build)"
|
||||
fi
|
||||
|
||||
# --- Verify binaries exist ----------------------------------------------------
|
||||
echo "[2/3] Verifying binaries"
|
||||
if [[ ! -f "$SIGNER_BIN" ]]; then
|
||||
echo "ERROR: Signer binary not found: $SIGNER_BIN"
|
||||
echo " Run without --no-build, or run build_static.sh first."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$SIGNER_BIN" ]]; then
|
||||
echo "ERROR: Signer binary is not executable: $SIGNER_BIN"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$CLIENT_BIN" ]]; then
|
||||
echo "ERROR: Client binary not found: $CLIENT_BIN"
|
||||
echo " Run without --no-build, or run build_static.sh first."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$CLIENT_BIN" ]]; then
|
||||
echo "ERROR: Client binary is not executable: $CLIENT_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " OK: $SIGNER_BIN ($(du -h "$SIGNER_BIN" | cut -f1))"
|
||||
echo " OK: $CLIENT_BIN ($(du -h "$CLIENT_BIN" | cut -f1))"
|
||||
|
||||
# Quick smoke test
|
||||
SIGNER_VERSION="$("$SIGNER_BIN" --version 2>&1 || echo "unknown")"
|
||||
echo " Signer version: $SIGNER_VERSION"
|
||||
|
||||
# --- Install ------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "[3/3] Installing to $INSTALL_PREFIX"
|
||||
|
||||
SUDO=""
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
echo "ERROR: Need root privileges to write to $INSTALL_PREFIX but sudo is not available"
|
||||
exit 1
|
||||
fi
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
$SUDO install -m 0755 "$SIGNER_BIN" "$INSTALL_PREFIX/nsigner"
|
||||
$SUDO install -m 0755 "$CLIENT_BIN" "$INSTALL_PREFIX/nsigner_client"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Deploy complete!"
|
||||
echo "=========================================="
|
||||
echo " $INSTALL_PREFIX/nsigner"
|
||||
echo " $INSTALL_PREFIX/nsigner_client"
|
||||
echo ""
|
||||
echo "Verify:"
|
||||
echo " nsigner --version"
|
||||
echo " nsigner_client --help"
|
||||
@@ -0,0 +1,736 @@
|
||||
# Agent Client Reference — n_signer
|
||||
|
||||
This document is the complete reference for **agents, AI tools, and automated processes** that need to call [`n_signer`](../README.md) — a signing oracle that holds BIP-39 keys in locked memory and exposes them over multiple transports.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture overview](#1-architecture-overview)
|
||||
2. [Wire protocol (all transports)](#2-wire-protocol-all-transports)
|
||||
3. [Transport: Qubes qrexec (recommended for cross-qube)](#3-transport-qubes-qrexec-recommended-for-cross-qube)
|
||||
4. [Transport: Local Unix abstract socket](#4-transport-local-unix-abstract-socket)
|
||||
5. [Transport: TCP (FIPS mesh)](#5-transport-tcp-fips-mesh)
|
||||
6. [Transport: HTTP](#6-transport-http)
|
||||
7. [Transport: USB/serial (hardware signers)](#7-transport-usbserial-hardware-signers)
|
||||
8. [Transport: Stdio (one-shot)](#8-transport-stdio-one-shot)
|
||||
9. [Auth envelope (kind-27235)](#9-auth-envelope-kind-27235)
|
||||
10. [Complete verb reference](#10-complete-verb-reference)
|
||||
11. [Algorithm reference](#11-algorithm-reference)
|
||||
12. [Error codes](#12-error-codes)
|
||||
13. [End-to-end: publish a Nostr event](#13-end-to-end-publish-a-nostr-event)
|
||||
14. [End-to-end: sign arbitrary data](#14-end-to-end-sign-arbitrary-data)
|
||||
15. [End-to-end: PQ KEM encapsulate/decapsulate](#15-end-to-end-pq-kem-encapsulatedecapsulate)
|
||||
16. [Discovery: finding running signers](#16-discovery-finding-running-signers)
|
||||
17. [Prerequisites and setup](#17-prerequisites-and-setup)
|
||||
18. [Reference files in this repo](#18-reference-files-in-this-repo)
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Caller (agent) │
|
||||
│ Python / Node / C / Shell / any language │
|
||||
│ Builds framed JSON-RPC → sends over transport → reads resp │
|
||||
└──────────┬──────────────────────────────────────┬───────────┘
|
||||
│ │
|
||||
┌─────┴──────┐ ┌──────────┴──────────┐
|
||||
│ qrexec │ │ TCP / HTTP / Unix │
|
||||
│ (Qubes) │ │ Socket / USB │
|
||||
└─────┬──────┘ └──────────┬──────────┘
|
||||
│ │
|
||||
┌─────┴──────────────────────────────────────┴───────────┐
|
||||
│ n_signer │
|
||||
│ BIP-39 mnemonic in mlock'd RAM │
|
||||
│ Derives keys on demand (secp256k1, ed25519, PQ, etc.) │
|
||||
│ Enforces policy + approval per caller │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The signer is a **single foreground process** attached to a terminal. It holds the mnemonic in locked memory only — nothing touches disk. When the process exits, all state is destroyed.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wire protocol (all transports)
|
||||
|
||||
Every transport uses the same framing and JSON-RPC contract.
|
||||
|
||||
### 2.1 Framing
|
||||
|
||||
```
|
||||
[4 bytes: big-endian payload length N][N bytes: UTF-8 JSON payload]
|
||||
```
|
||||
|
||||
- Length prefix is `uint32_t` in network byte order.
|
||||
- Payload is exactly `N` bytes of UTF-8 JSON.
|
||||
- One request frame → one response frame per connection (except HTTP which uses standard HTTP request/response).
|
||||
|
||||
### 2.2 Request shape
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<caller-supplied string, echoed in response>",
|
||||
"method": "<verb name>",
|
||||
"params": [ <arg0>, <arg1>, ..., { <options> } ]
|
||||
}
|
||||
```
|
||||
|
||||
- `id` — any string. Used to match async responses. Echoed verbatim.
|
||||
- `method` — one of the verbs in §10.
|
||||
- `params` — JSON array. Positional args first; last element is conventionally an options object.
|
||||
|
||||
### 2.3 Response shape
|
||||
|
||||
Success:
|
||||
```json
|
||||
{ "id": "<string>", "result": "<value>" }
|
||||
```
|
||||
|
||||
Error:
|
||||
```json
|
||||
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
`result` is always a JSON string. For structured verbs (like `get_public_key` with algorithm), the string is itself serialized JSON — parse it again.
|
||||
|
||||
### 2.4 Auth envelope (required for TCP/HTTP, optional for qrexec)
|
||||
|
||||
TCP and HTTP listeners require a kind-27235 auth envelope in the `auth` field. See [§9](#9-auth-envelope-kind-27235).
|
||||
|
||||
---
|
||||
|
||||
## 3. Transport: Qubes qrexec (recommended for cross-qube)
|
||||
|
||||
**Best for**: Agents running in a caller qube, talking to n_signer in a dedicated signer qube.
|
||||
|
||||
**Auth**: Not required — caller identity comes from `QREXEC_REMOTE_DOMAIN` as `qubes:<source-vm>`.
|
||||
|
||||
### 3.1 Shell one-liner
|
||||
|
||||
```bash
|
||||
# Get public key for nostr_index 0
|
||||
printf '\x00\x00\x00\x3f'"$(echo '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}')" | \
|
||||
qrexec-client-vm nostr_signer qubes.NsignerRpc | tail -c +5
|
||||
```
|
||||
|
||||
### 3.2 Python (stdlib only)
|
||||
|
||||
```python
|
||||
import json, struct, subprocess
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
"""Send one framed JSON-RPC request via qrexec, return parsed response."""
|
||||
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 failed: {err.decode()}")
|
||||
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
return json.loads(out[4:4+length])
|
||||
```
|
||||
|
||||
Full demo: [`client/demo_python.py`](../client/demo_python.py)
|
||||
|
||||
### 3.3 Node.js
|
||||
|
||||
```javascript
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
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 chunks = [];
|
||||
proc.stdout.on("data", (c) => chunks.push(c));
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) return reject(new Error(`exit code ${code}`));
|
||||
const buf = Buffer.concat(chunks);
|
||||
const len = buf.readUInt32BE(0);
|
||||
resolve(JSON.parse(buf.subarray(4, 4 + len).toString()));
|
||||
});
|
||||
proc.stdin.write(framed);
|
||||
proc.stdin.end();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Full demo: [`client/demo_javascript.js`](../client/demo_javascript.js)
|
||||
|
||||
### 3.4 C (using nostr_core_lib)
|
||||
|
||||
```c
|
||||
#include "nostr_signer.h"
|
||||
|
||||
nostr_signer_t *signer = nostr_signer_nsigner_qrexec("nostr_signer", "qubes.NsignerRpc", NULL, 30000);
|
||||
nostr_signer_nsigner_set_nostr_index(signer, 0);
|
||||
char pubkey[65];
|
||||
nostr_signer_get_public_key(signer, pubkey);
|
||||
```
|
||||
|
||||
Full demo: [`client/demo_c99.c`](../client/demo_c99.c)
|
||||
|
||||
### 3.5 Prerequisites
|
||||
|
||||
1. n_signer running in the signer qube: `nsigner --listen unix --socket-name nsigner --bridge-source-trusted`
|
||||
2. qrexec service installed at `/etc/qubes-rpc/qubes.NsignerRpc`: `exec nsigner bridge --to nsigner`
|
||||
3. dom0 policy allowing caller qube → signer qube (see [`packaging/qubes/policy.d/40-nsigner.policy`](../packaging/qubes/policy.d/40-nsigner.policy))
|
||||
|
||||
---
|
||||
|
||||
## 4. Transport: Local Unix abstract socket
|
||||
|
||||
**Best for**: Agents on the same machine as the signer.
|
||||
|
||||
**Auth**: Not required — identity from `SO_PEERCRED` (kernel-verified UID/PID).
|
||||
|
||||
### 4.1 Discovery
|
||||
|
||||
```bash
|
||||
nsigner list
|
||||
# Output: @nsigner, @nsigner_hairy_dog, etc.
|
||||
```
|
||||
|
||||
### 4.2 Using the nsigner CLI
|
||||
|
||||
```bash
|
||||
nsigner client '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}' --socket-name nsigner
|
||||
```
|
||||
|
||||
### 4.3 C (using nostr_core_lib)
|
||||
|
||||
```c
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
|
||||
nsigner_transport_t *t = nsigner_transport_open_unix("nsigner", 5000);
|
||||
nsigner_client_t *c = nsigner_client_new(t);
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
cJSON *result = NULL;
|
||||
nsigner_client_call(c, "nostr_get_public_key", params, &result);
|
||||
```
|
||||
|
||||
See [`examples/get_public_key_client.c`](../examples/get_public_key_client.c).
|
||||
|
||||
### 4.4 Python (raw socket)
|
||||
|
||||
```python
|
||||
import json, socket, struct
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect("\0nsigner") # \0 prefix for abstract namespace
|
||||
|
||||
payload = json.dumps({"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}).encode()
|
||||
sock.sendall(struct.pack(">I", len(payload)) + payload)
|
||||
|
||||
hdr = sock.recv(4)
|
||||
length = struct.unpack(">I", hdr)[0]
|
||||
body = sock.recv(length)
|
||||
print(json.loads(body))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Transport: TCP (FIPS mesh)
|
||||
|
||||
**Best for**: Cross-qube or network callers where qrexec is not available.
|
||||
|
||||
**Auth**: **Required** — kind-27235 auth envelope (see [§9](#9-auth-envelope-kind-27235)).
|
||||
|
||||
### 5.1 Python (with coincurve)
|
||||
|
||||
```python
|
||||
import hashlib, json, socket, struct, time
|
||||
from coincurve import PrivateKey
|
||||
|
||||
HOST, PORT = "192.168.1.100", 11111
|
||||
CALLER_PRIVKEY = bytes(range(1, 33)) # Replace with your key
|
||||
|
||||
params = [{"nostr_index": 0}]
|
||||
body_hash = hashlib.sha256(json.dumps(params, separators=(",",":")).encode()).hexdigest()
|
||||
|
||||
sk = PrivateKey(CALLER_PRIVKEY)
|
||||
pubkey_x = sk.public_key.format(compressed=False)[1:33].hex()
|
||||
created_at = int(time.time())
|
||||
|
||||
tags = [["nsigner_rpc","1"],["nsigner_method","get_public_key"],["nsigner_body_hash",body_hash]]
|
||||
serialized = json.dumps([0, pubkey_x, created_at, 27235, tags, "tcp-agent"], separators=(",",":")).encode()
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00"*32).hex()
|
||||
|
||||
request = {
|
||||
"id": "1", "method": "get_public_key", "params": params,
|
||||
"auth": {"id": event_id, "pubkey": pubkey_x, "created_at": created_at,
|
||||
"kind": 27235, "tags": tags, "content": "tcp-agent", "sig": sig},
|
||||
}
|
||||
|
||||
payload = json.dumps(request, separators=(",",":")).encode()
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
|
||||
with socket.create_connection((HOST, PORT), timeout=10) as s:
|
||||
s.sendall(frame)
|
||||
hdr = s.recv(4)
|
||||
ln = struct.unpack(">I", hdr)[0]
|
||||
body = s.recv(ln)
|
||||
|
||||
print(json.loads(body))
|
||||
```
|
||||
|
||||
See [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js).
|
||||
|
||||
### 5.2 C (using nostr_core_lib)
|
||||
|
||||
```c
|
||||
nsigner_transport_t *t = nsigner_transport_open_tcp("192.168.1.100", 11111, 10000);
|
||||
nsigner_client_t *c = nsigner_client_new(t);
|
||||
nsigner_client_set_auth(c, caller_privkey, "tcp-agent");
|
||||
// ... call as usual
|
||||
```
|
||||
|
||||
See [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c).
|
||||
|
||||
---
|
||||
|
||||
## 6. Transport: HTTP
|
||||
|
||||
**Best for**: curl-friendly callers, REST clients.
|
||||
|
||||
**Auth**: **Required** — kind-27235 auth envelope in the JSON body.
|
||||
|
||||
```bash
|
||||
curl -X POST http://192.168.1.100:11112/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"1",
|
||||
"method":"get_public_key",
|
||||
"params":[{"nostr_index":0}],
|
||||
"auth":{...}
|
||||
}'
|
||||
```
|
||||
|
||||
The HTTP listener uses the same framing internally but presents a standard HTTP interface. One request per connection.
|
||||
|
||||
---
|
||||
|
||||
## 7. Transport: USB/serial (hardware signers)
|
||||
|
||||
**Best for**: Embedded/air-gap signers (Feather S3, Teensy 4.1, CYD ESP32).
|
||||
|
||||
**Auth**: Not required — physical possession is the trust anchor.
|
||||
|
||||
### 7.1 Python (pyserial)
|
||||
|
||||
```python
|
||||
import json, struct, serial
|
||||
|
||||
ser = serial.Serial("/dev/ttyACM0", 115200, timeout=5)
|
||||
|
||||
payload = json.dumps({"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}).encode()
|
||||
ser.write(struct.pack(">I", len(payload)) + payload)
|
||||
|
||||
hdr = ser.read(4)
|
||||
length = struct.unpack(">I", hdr)[0]
|
||||
body = ser.read(length)
|
||||
print(json.loads(body))
|
||||
```
|
||||
|
||||
See [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and [`examples/feather_sign_event.py`](../examples/feather_sign_event.py).
|
||||
|
||||
---
|
||||
|
||||
## 8. Transport: Stdio (one-shot)
|
||||
|
||||
**Best for**: Scripted one-off calls via pipe.
|
||||
|
||||
The signer can be started with `--listen stdio` to handle exactly one framed request on stdin and write one framed response to stdout.
|
||||
|
||||
```bash
|
||||
echo -n '<framed request>' | nsigner --listen stdio --allow-all
|
||||
```
|
||||
|
||||
Or via the qrexec bridge service (which uses stdio internally to relay to the persistent signer's Unix socket).
|
||||
|
||||
---
|
||||
|
||||
## 9. Auth envelope (kind-27235)
|
||||
|
||||
TCP and HTTP listeners require a **kind-27235 Nostr event** in the `auth` field of every request. This proves the caller controls a keypair.
|
||||
|
||||
### 9.1 Auth envelope structure
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"id": "<sha256 of serialized event>",
|
||||
"pubkey": "<caller's secp256k1 x-only pubkey hex>",
|
||||
"created_at": <unix timestamp>,
|
||||
"kind": 27235,
|
||||
"tags": [
|
||||
["nsigner_rpc", "<request id>"],
|
||||
["nsigner_method", "<method name>"],
|
||||
["nsigner_body_hash", "<sha256 of canonical params JSON>"]
|
||||
],
|
||||
"content": "<arbitrary string>",
|
||||
"sig": "<schnorr signature hex>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Building the auth envelope (pseudocode)
|
||||
|
||||
```
|
||||
1. Compute body_hash = SHA256(JSON.stringify(params, separators=(",",":")))
|
||||
2. Build tags: [["nsigner_rpc", id], ["nsigner_method", method], ["nsigner_body_hash", body_hash]]
|
||||
3. Serialize event for signing: JSON.stringify([0, pubkey, created_at, 27235, tags, content])
|
||||
4. Compute event_id = SHA256(serialized)
|
||||
5. Sign event_id with caller's secp256k1 key (Schnorr/BIP-340)
|
||||
6. Include full event as the "auth" field in the request
|
||||
```
|
||||
|
||||
### 9.3 Validation rules (server-side)
|
||||
|
||||
| Check | Error code |
|
||||
|-------|-----------|
|
||||
| Missing `auth` field | 2014 `auth_envelope_required` |
|
||||
| Malformed auth JSON | 2010 `auth_envelope_malformed` |
|
||||
| `kind` != 27235 | 2013 `auth_kind_invalid` |
|
||||
| Signature doesn't verify | 2012 `auth_signature_invalid` |
|
||||
| `nsigner_rpc` tag != request `id` | 2011 `auth_body_mismatch` |
|
||||
| `nsigner_method` tag != request `method` | 2011 `auth_body_mismatch` |
|
||||
| `nsigner_body_hash` != SHA256(params) | 2011 `auth_body_mismatch` |
|
||||
| Timestamp skew > 300 seconds | 2015 `auth_envelope_mismatch` |
|
||||
| Replay (same event_id seen before) | 2015 `auth_envelope_mismatch` |
|
||||
|
||||
### 9.4 Python example (full)
|
||||
|
||||
See [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) for a complete working example.
|
||||
|
||||
### 9.5 JavaScript example (full)
|
||||
|
||||
See [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) for a complete working example.
|
||||
|
||||
---
|
||||
|
||||
## 10. Complete verb reference
|
||||
|
||||
### 10.1 Algorithm-based verbs
|
||||
|
||||
These select a key by `algorithm` + `index` (see [§11](#11-algorithm-reference) for derivation paths).
|
||||
|
||||
| Verb | Algorithms | Params | Options |
|
||||
|------|-----------|--------|---------|
|
||||
| `get_public_key` | All key-deriving | `[]` | `algorithm`, `index` |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `[<message_hex>]` | `algorithm`, `index`, `scheme`* |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `[<message_hex>, <sig_hex>]` | `algorithm`, `index`, `scheme`* |
|
||||
| `encapsulate` | ml-kem-768 | `[<peer_pubkey_hex>]` | `algorithm` |
|
||||
| `decapsulate` | ml-kem-768 | `[<ciphertext_hex>]` | `algorithm`, `index` |
|
||||
| `derive_shared_secret` | x25519 | `[<peer_pubkey_hex>]` | `algorithm`, `index` |
|
||||
| `derive` | secp256k1 | `[<data>]` | `algorithm`, `index` (required) |
|
||||
| `encrypt` | otp | `[<plaintext_base64>]` | `algorithm`, `encoding` |
|
||||
| `decrypt` | otp | `[<ciphertext>]` | `algorithm`, `encoding` |
|
||||
|
||||
\* `scheme`: `"schnorr"` (default) or `"ecdsa"` for secp256k1.
|
||||
|
||||
### 10.2 Nostr protocol verbs
|
||||
|
||||
These select a secp256k1 NIP-06 key via `nostr_index` (or `role`/`role_path`).
|
||||
|
||||
| Verb | Params | Options |
|
||||
|------|--------|---------|
|
||||
| `nostr_get_public_key` | `[]` | `nostr_index`, `format` |
|
||||
| `nostr_sign_event` | `[<event_json>]` | `nostr_index` |
|
||||
| `nostr_mine_event` | `[<event_json>]` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` |
|
||||
| `nostr_nip04_encrypt` | `[<peer_pubkey_hex>, <plaintext>]` | `nostr_index` |
|
||||
| `nostr_nip04_decrypt` | `[<peer_pubkey_hex>, <ciphertext>]` | `nostr_index` |
|
||||
| `nostr_nip44_encrypt` | `[<peer_pubkey_hex>, <plaintext>]` | `nostr_index` |
|
||||
| `nostr_nip44_decrypt` | `[<peer_pubkey_hex>, <ciphertext>]` | `nostr_index` |
|
||||
|
||||
### 10.3 Metadata
|
||||
|
||||
| Verb | Params | Description |
|
||||
|------|--------|-------------|
|
||||
| `get_info` | `[]` | Returns signer metadata (name, version, supported verbs/algorithms). Safe to call before mnemonic is loaded. |
|
||||
|
||||
### 10.4 Example requests
|
||||
|
||||
```json
|
||||
// Get public key for ML-DSA-65 index 0
|
||||
{"id":"1","method":"get_public_key","params":[{"algorithm":"ml-dsa-65","index":0}]}
|
||||
|
||||
// Sign "hello" with ed25519 index 0
|
||||
{"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}
|
||||
|
||||
// Sign a Nostr event with nostr_index 0
|
||||
{"id":"3","method":"nostr_sign_event","params":["{\"kind\":1,\"content\":\"Hello\",\"tags\":[],\"created_at\":1700000000,\"pubkey\":\"<hex>\"}",{"nostr_index":0}]}
|
||||
|
||||
// NIP-44 encrypt
|
||||
{"id":"4","method":"nostr_nip44_encrypt","params":["<peer_pubkey_hex>","secret message",{"nostr_index":0}]}
|
||||
|
||||
// ML-KEM-768 encapsulate
|
||||
{"id":"5","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]}
|
||||
|
||||
// Derive HMAC-SHA256(privkey, data) for opaque identifiers
|
||||
{"id":"6","method":"derive","params":["<data_hex>",{"algorithm":"secp256k1","index":0}]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Algorithm reference
|
||||
|
||||
| Algorithm | Key type | FIPS | Derivation path | Pubkey | Privkey | Signature |
|
||||
|-----------|----------|------|-----------------|--------|---------|-----------|
|
||||
| `secp256k1` | Signature (Nostr) | — | `m/44'/1237'/<n>'/0/0` | 32 B | 32 B | 64 B |
|
||||
| `ed25519` | Signature (SSH) | — | `m/44'/102001'/<n>'/0/0'` | 32 B | 32 B | 64 B |
|
||||
| `x25519` | Key agreement (age) | — | `m/44'/102002'/<n>'/0/0'` | 32 B | 32 B | — |
|
||||
| `ml-dsa-65` | PQ signature | FIPS 204 | DRBG from seed | 1952 B | 4032 B | 3309 B |
|
||||
| `slh-dsa-128s` | PQ hash-based sig | FIPS 205 | DRBG from seed | 32 B | 64 B | 7856 B |
|
||||
| `ml-kem-768` | PQ KEM | FIPS 203 | DRBG from seed | 1184 B | 2400 B | — |
|
||||
| `otp` | One-time pad | — | USB pad (no derivation) | — | — | — |
|
||||
|
||||
### 11.1 `get_public_key` response format
|
||||
|
||||
Algorithm-based `get_public_key` returns structured JSON:
|
||||
|
||||
```json
|
||||
{"algorithm":"ml-dsa-65","public_key":"<hex>","key_id":"<first 16 hex chars of pubkey>"}
|
||||
```
|
||||
|
||||
Nostr `nostr_get_public_key` returns a plain 64-hex-char string by default, or structured JSON with `{"format":"structured"}`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Error codes
|
||||
|
||||
| Code | Message | Meaning |
|
||||
|------|---------|---------|
|
||||
| -32700 | `parse_error` | Request is not valid JSON |
|
||||
| -32600 | `invalid_request` | Missing `id`, `method`, or `params` |
|
||||
| -32601 | `method_not_found` | Unknown verb |
|
||||
| -32602 | `invalid_params` | Malformed arguments |
|
||||
| 1001 | `ambiguous_role_selector` | Multiple role selectors given |
|
||||
| 1002 | `unknown_role` | No role matched selector |
|
||||
| 1003 | `no_default_role` | No selector and no `main` role |
|
||||
| 1004 | `purpose_mismatch` | Role purpose not valid for verb |
|
||||
| 1005 | `curve_mismatch` | Role curve not valid for verb |
|
||||
| 1006 | `mnemonic_not_loaded` | No mnemonic loaded |
|
||||
| 1007 | `no_termination_condition` | `nostr_mine_event` without difficulty/timeout |
|
||||
| 1008 | `mining_failed` | Internal PoW error |
|
||||
| 1009 | `not_yet_implemented` | Verb+algorithm not yet implemented |
|
||||
| 1010 | `algorithm_not_supported_for_verb` | Algorithm not valid for verb |
|
||||
| 2010 | `auth_envelope_malformed` | Auth JSON is malformed |
|
||||
| 2011 | `auth_body_mismatch` | Auth tags don't match request |
|
||||
| 2012 | `auth_signature_invalid` | Auth signature doesn't verify |
|
||||
| 2013 | `auth_kind_invalid` | Auth kind != 27235 |
|
||||
| 2014 | `auth_envelope_required` | Auth missing (TCP/HTTP) |
|
||||
| 2015 | `auth_envelope_mismatch` | Timestamp skew or replay |
|
||||
|
||||
---
|
||||
|
||||
## 13. End-to-end: publish a Nostr event
|
||||
|
||||
This is the most common agent task. Here's the complete flow using qrexec (simplest cross-qube transport):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Publish a Nostr event via n_signer over qrexec."""
|
||||
import json, struct, subprocess, sys, time
|
||||
|
||||
def call_nsigner(target_qube, request):
|
||||
payload = json.dumps(request, separators=(",", ":")).encode()
|
||||
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, _ = proc.communicate(frame)
|
||||
length = struct.unpack(">I", out[:4])[0]
|
||||
return json.loads(out[4:4+length])
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "nostr_signer"
|
||||
idx = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
|
||||
# Step 1: Get public key
|
||||
pub = call_nsigner(target, {"id":"1","method":"get_public_key","params":[{"nostr_index":idx}]})["result"]
|
||||
print(f"Pubkey: {pub}")
|
||||
|
||||
# Step 2: Build and sign event
|
||||
event = {
|
||||
"kind": 1,
|
||||
"content": "Hello from my agent!",
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"pubkey": pub,
|
||||
}
|
||||
result = call_nsigner(target, {
|
||||
"id": "2",
|
||||
"method": "nostr_sign_event",
|
||||
"params": [json.dumps(event, separators=(",",":")), {"nostr_index": idx}],
|
||||
})
|
||||
signed = json.loads(result["result"])
|
||||
print(f"Event ID: {signed['id']}")
|
||||
print(f"Signature: {signed['sig']}")
|
||||
|
||||
# Step 3: Broadcast to relay(s)
|
||||
# signed is a complete Nostr event with id and sig — send to any relay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. End-to-end: sign arbitrary data
|
||||
|
||||
Using algorithm-based verbs (any algorithm, any index):
|
||||
|
||||
```python
|
||||
# Sign "hello" with ed25519 index 0
|
||||
result = call_nsigner(target, {
|
||||
"id": "1",
|
||||
"method": "sign",
|
||||
"params": ["68656c6c6f", {"algorithm": "ed25519", "index": 0}],
|
||||
})
|
||||
signature = result["result"] # hex string
|
||||
|
||||
# Verify
|
||||
result = call_nsigner(target, {
|
||||
"id": "2",
|
||||
"method": "verify",
|
||||
"params": ["68656c6c6f", signature, {"algorithm": "ed25519", "index": 0}],
|
||||
})
|
||||
print(f"Verified: {result['result']}") # "true" or "false"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. End-to-end: PQ KEM encapsulate/decapsulate
|
||||
|
||||
```python
|
||||
# Get ML-KEM-768 public key for index 0
|
||||
pub_result = call_nsigner(target, {
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{"algorithm": "ml-kem-768", "index": 0}],
|
||||
})
|
||||
pub_info = json.loads(pub_result["result"])
|
||||
print(f"ML-KEM-768 pubkey: {pub_info['public_key'][:32]}...")
|
||||
|
||||
# Encapsulate (generate a shared secret + ciphertext for that pubkey)
|
||||
enc_result = call_nsigner(target, {
|
||||
"id": "2",
|
||||
"method": "encapsulate",
|
||||
"params": [pub_info["public_key"], {"algorithm": "ml-kem-768"}],
|
||||
})
|
||||
enc_data = json.loads(enc_result["result"])
|
||||
print(f"Ciphertext: {enc_data['ciphertext'][:32]}...")
|
||||
print(f"Shared secret: {enc_data['shared_secret'][:32]}...")
|
||||
|
||||
# Decapsulate (recover shared secret from ciphertext using private key)
|
||||
dec_result = call_nsigner(target, {
|
||||
"id": "3",
|
||||
"method": "decapsulate",
|
||||
"params": [enc_data["ciphertext"], {"algorithm": "ml-kem-768", "index": 0}],
|
||||
})
|
||||
dec_data = json.loads(dec_result["result"])
|
||||
print(f"Decapsulated secret: {dec_data['shared_secret'][:32]}...")
|
||||
assert dec_data["shared_secret"] == enc_data["shared_secret"]
|
||||
print("✓ KEM round-trip verified")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Discovery: finding running signers
|
||||
|
||||
### 16.1 List running signers
|
||||
|
||||
```bash
|
||||
nsigner list
|
||||
```
|
||||
|
||||
Output (one per line):
|
||||
```
|
||||
@nsigner
|
||||
@nsigner_hairy_dog
|
||||
@nsigner_brave_canyon
|
||||
```
|
||||
|
||||
### 16.2 Check if a specific signer is running
|
||||
|
||||
```bash
|
||||
nsigner list | grep -q @nsigner && echo "running" || echo "not running"
|
||||
```
|
||||
|
||||
### 16.3 Programmatic discovery (Python)
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
result = subprocess.run(["nsigner", "list"], capture_output=True, text=True)
|
||||
signers = [s.strip() for s in result.stdout.split("\n") if s.strip()]
|
||||
print(f"Found {len(signers)} signer(s): {signers}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Prerequisites and setup
|
||||
|
||||
### 17.1 On the signer qube
|
||||
|
||||
```bash
|
||||
# Install nsigner
|
||||
# Start the persistent signer
|
||||
nsigner --listen unix --socket-name nsigner --bridge-source-trusted
|
||||
|
||||
# Install qrexec service
|
||||
sudo cp packaging/qubes/rpc/qubes.NsignerRpc /etc/qubes-rpc/qubes.NsignerRpc
|
||||
sudo chmod 0755 /etc/qubes-rpc/qubes.NsignerRpc
|
||||
```
|
||||
|
||||
### 17.2 In dom0
|
||||
|
||||
```bash
|
||||
# Install policy
|
||||
sudo cp packaging/qubes/policy.d/40-nsigner.policy /etc/qubes/policy.d/40-nsigner.policy
|
||||
|
||||
# Tag the signer qube
|
||||
qvm-tags nostr_signer add nsigner-signer
|
||||
```
|
||||
|
||||
### 17.3 On the caller qube
|
||||
|
||||
No special setup needed — just `qrexec-client-vm` (pre-installed in all Qubes templates).
|
||||
|
||||
---
|
||||
|
||||
## 18. Reference files in this repo
|
||||
|
||||
| File | What it shows |
|
||||
|------|---------------|
|
||||
| [`client/demo_python.py`](../client/demo_python.py) | Full Python demo (qrexec, stdlib only) — get_public_key, sign_event, nip44, mine_event |
|
||||
| [`client/demo_javascript.js`](../client/demo_javascript.js) | Full Node.js demo (qrexec) — same operations |
|
||||
| [`client/demo_c99.c`](../client/demo_c99.c) | Full C99 demo (qrexec, nostr_core_lib) — same operations |
|
||||
| [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) | Minimal TCP/FIPS with auth envelope (Python) |
|
||||
| [`examples/get_pubkey_qrexec.c`](../examples/get_pubkey_qrexec.c) | Minimal qrexec in C |
|
||||
| [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) | Minimal TCP in C with auth envelope |
|
||||
| [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) | TCP/FIPS in Node.js with auth envelope |
|
||||
| [`examples/n_signer_qube_example_qrexec.js`](../examples/n_signer_qube_example_qrexec.js) | qrexec in Node.js |
|
||||
| [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) | Unix socket in C |
|
||||
| [`examples/sign_event_client.c`](../examples/sign_event_client.c) | Unix socket sign event in C |
|
||||
| [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) | USB/serial hardware signer (Python) |
|
||||
| [`examples/feather_sign_event.py`](../examples/feather_sign_event.py) | USB/serial sign event (Python) |
|
||||
| [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) | ML-DSA-65 sign in C |
|
||||
| [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) | ML-KEM-768 encaps/decaps in C |
|
||||
| [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) | ed25519 SSH sign in C |
|
||||
| [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md) | Full wire contract spec (733 lines) |
|
||||
| [`README.md`](../README.md) | Full n_signer documentation (741 lines) |
|
||||
| [`packaging/qubes/setup_signer_qube.sh`](../packaging/qubes/setup_signer_qube.sh) | Automated signer qube setup |
|
||||
| [`.roo/n_signer_client.md`](../.roo/n_signer_client.md) | Roo skill file (agent-invocable) |
|
||||
@@ -127,20 +127,38 @@ Methods are NIP-46 style verbs.
|
||||
- `nip44_encrypt`
|
||||
- `nip44_decrypt`
|
||||
|
||||
### 4.2b Algorithm-based verbs (new)
|
||||
|
||||
In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly:
|
||||
|
||||
- `sign` — sign arbitrary bytes (params: `[message_hex, {algorithm, index, scheme?}]`)
|
||||
- `verify` — verify a signature (params: `[message_hex, signature_hex, {algorithm, index, scheme?}]`)
|
||||
- `encapsulate` — KEM encapsulation (params: `[peer_pubkey_hex, {algorithm}]`)
|
||||
- `decapsulate` — KEM decapsulation (params: `[ciphertext_hex, {algorithm, index}]`)
|
||||
- `derive_shared_secret` — ECDH key agreement (params: `[peer_pubkey_hex, {algorithm, index}]`)
|
||||
- `derive` — `HMAC-SHA256(privkey, data)` key-derived MAC (params: `[data, {algorithm:"secp256k1", index}]`; `index` required). Returns `{algorithm, key_id, digest}` where `digest` is 64 hex chars. Use for deterministic opaque identifiers (e.g. NIP-33 `d` tags) keyed by the derived private key.
|
||||
- `get_public_key` with `algorithm` parameter — returns structured JSON
|
||||
|
||||
Algorithm names: `secp256k1`, `ed25519`, `ml-dsa-65`, `slh-dsa-128s`, `x25519`, `ml-kem-768`
|
||||
|
||||
For secp256k1 `sign`/`verify`, the optional `scheme` parameter selects `"schnorr"` (default, BIP-340) or `"ecdsa"`.
|
||||
|
||||
Old verb aliases (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) map to the new verbs when used with the `algorithm` parameter. Without `algorithm`, they fall through to the role-based path.
|
||||
|
||||
See [README.md §4c](../README.md) for full details.
|
||||
|
||||
### 4.3 Selector options
|
||||
|
||||
The last param may include selector options:
|
||||
|
||||
- `role`
|
||||
- `nostr_index`
|
||||
- `role_path`
|
||||
- `role` — name of a pre-registered role entry
|
||||
- `role_path` — full BIP-44 derivation path
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. `role`
|
||||
2. `nostr_index`
|
||||
3. `role_path`
|
||||
4. default role `main`
|
||||
2. `role_path`
|
||||
3. default role `main`
|
||||
|
||||
Conflicting selector fields must be rejected as `ambiguous_role_selector`.
|
||||
|
||||
@@ -437,7 +455,7 @@ Request:
|
||||
"method": "sign_event",
|
||||
"params": [
|
||||
"<event_json>",
|
||||
{ "role": "main", "nostr_index": 0 }
|
||||
{ "role": "main", "role_path": "m/44'/1237'/0'/0/0" }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -484,7 +502,228 @@ Decrypt response:
|
||||
|
||||
---
|
||||
|
||||
## 11. Compatibility notes
|
||||
## 11. Post-Quantum and Multi-Algorithm Support
|
||||
|
||||
n_signer supports six cryptographic algorithms, all derived deterministically
|
||||
from the same BIP-39 mnemonic via distinct derivation paths:
|
||||
|
||||
| Algorithm | Purpose | Curve string | Purpose string | Derivation path |
|
||||
|---|---|---|---|---|
|
||||
| `secp256k1` | Nostr (sign_event, NIP-04/44) | `secp256k1` | `nostr` | `m/44'/1237'/<n>'/0/0` (NIP-06) |
|
||||
| `ed25519` | SSH signing, general signatures | `ed25519` | `ssh` | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) |
|
||||
| `x25519` | Key agreement (age, ECDH) | `x25519` | `age` | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) |
|
||||
| `ml-dsa-65` | Post-quantum signatures (FIPS 204) | `ml-dsa-65` | `pq-sig` | `m/44'/102003'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
| `slh-dsa-128s` | Post-quantum hash-based signatures (FIPS 205) | `slh-dsa-128s` | `pq-sig` | `m/44'/102004'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
| `ml-kem-768` | Post-quantum key encapsulation (FIPS 203) | `ml-kem-768` | `pq-kem` | `m/44'/102005'/<n>'/0'/0'` → seed → PQClean keygen |
|
||||
|
||||
The `102XXX` coin types are unregistered in SLIP-44 and reserved by n_signer
|
||||
for PQ/SSH/age algorithm families. All non-secp256k1 paths use SLIP-0010
|
||||
all-hardened derivation.
|
||||
|
||||
### 11.1 Algorithm key sizes
|
||||
|
||||
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | 32 bytes | 32 bytes | 64 bytes | — | — |
|
||||
| ed25519 | 32 bytes | 32 bytes | 64 bytes | — | — |
|
||||
| x25519 | 32 bytes | 32 bytes | — | — | 32 bytes |
|
||||
| ML-DSA-65 | 1952 bytes | 4032 bytes | 3309 bytes | — | — |
|
||||
| SLH-DSA-128s | 32 bytes | 64 bytes | 7856 bytes | — | — |
|
||||
| ML-KEM-768 | 1184 bytes | 2400 bytes | — | 1088 bytes | 32 bytes |
|
||||
|
||||
PQ public keys and signatures are much larger than classical ones. Clients
|
||||
must allocate buffers accordingly (ML-DSA-65 pubkey hex = 3904 chars;
|
||||
SLH-DSA-128s signature hex = 15712 chars; ML-KEM-768 pubkey hex = 2368 chars).
|
||||
|
||||
### 11.2 New verbs
|
||||
|
||||
| Verb | Purpose | Allowed (purpose, curve) | Description |
|
||||
|---|---|---|---|
|
||||
| `sign_data` | pq-sig, ssh | (pq-sig, ml-dsa-65), (pq-sig, slh-dsa-128s), (ssh, ed25519) | Sign arbitrary bytes (not a Nostr event) |
|
||||
| `verify_signature` | pq-sig, ssh | same as `sign_data` | Verify a signature against the role's public key |
|
||||
| `ssh_sign` | ssh | (ssh, ed25519) | Sign an SSH authentication challenge (ed25519) |
|
||||
| `kem_encapsulate` | pq-kem | (pq-kem, ml-kem-768) | Encapsulate: generate ciphertext + shared secret from a peer's ML-KEM public key |
|
||||
| `kem_decapsulate` | pq-kem | (pq-kem, ml-kem-768) | Decapsulate: recover shared secret from ciphertext using the role's ML-KEM private key |
|
||||
|
||||
The existing Nostr verbs (`sign_event`, `nip44_*`, `nip04_*`, `mine_event`)
|
||||
remain restricted to `purpose=nostr + curve=secp256k1`.
|
||||
|
||||
### 11.3 Structured `get_public_key` response format
|
||||
|
||||
`get_public_key` is a universal verb — it works for all six algorithms.
|
||||
|
||||
**For secp256k1 (backward compatibility):** the result is a plain hex string
|
||||
(the existing format). Existing Nostr clients are unaffected.
|
||||
|
||||
```json
|
||||
{ "id": "1", "result": "<64-char hex pubkey>" }
|
||||
```
|
||||
|
||||
**For secp256k1 with `format: "structured"` option:** new clients can request
|
||||
the structured format for consistency:
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{ "role": "main", "format": "structured" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"result": "{\"algorithm\":\"secp256k1\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}"
|
||||
}
|
||||
```
|
||||
|
||||
**For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s,
|
||||
ML-KEM-768):** the result is always a structured JSON object serialized as a
|
||||
string:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"result": {
|
||||
"algorithm": "ml-dsa-65",
|
||||
"public_key": "<hex-encoded public key>",
|
||||
"key_id": "<first 16 hex chars of public key>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `key_id` is the first 16 hex characters of the public key — a short
|
||||
display identifier similar to an SSH key fingerprint. The `result` field is a
|
||||
JSON string (the object serialized), so clients must parse it twice: once for
|
||||
the JSON-RPC envelope, once for the result object.
|
||||
|
||||
### 11.4 Example: `sign_data` (ML-DSA-65)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "10",
|
||||
"method": "sign_data",
|
||||
"params": ["68656c6c6f", { "role": "pq_sig" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "10",
|
||||
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ml-dsa-65\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the message bytes as hex. The signature is hex-encoded
|
||||
(3309 bytes = 6618 hex chars for ML-DSA-65).
|
||||
|
||||
### 11.5 Example: `verify_signature` (ed25519)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "11",
|
||||
"method": "verify_signature",
|
||||
"params": ["<msg_hex>", "<sig_hex>", { "role": "ssh_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "11", "result": "{\"valid\":true}" }
|
||||
```
|
||||
|
||||
The signature is verified against the role's derived public key.
|
||||
|
||||
### 11.6 Example: `ssh_sign` (ed25519)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"method": "ssh_sign",
|
||||
"params": ["<session_id_hex>", { "role": "ssh_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the SSH session ID (or challenge) as hex. The signature is
|
||||
a raw ed25519 signature (64 bytes = 128 hex chars).
|
||||
|
||||
### 11.7 Example: `kem_encapsulate` (ML-KEM-768)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "13",
|
||||
"method": "kem_encapsulate",
|
||||
"params": ["<peer_pubkey_hex>", { "role": "kem_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "13",
|
||||
"result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the peer's ML-KEM-768 public key as hex (1184 bytes = 2368
|
||||
hex chars). The response contains the ciphertext (1088 bytes = 2176 hex chars)
|
||||
and the shared secret (32 bytes = 64 hex chars). The encapsulating party keeps
|
||||
the shared secret; the ciphertext is sent to the decapsulating party.
|
||||
|
||||
### 11.8 Example: `kem_decapsulate` (ML-KEM-768)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"id": "14",
|
||||
"method": "kem_decapsulate",
|
||||
"params": ["<ciphertext_hex>", { "role": "kem_main" }]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "14",
|
||||
"result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}"
|
||||
}
|
||||
```
|
||||
|
||||
The first param is the ciphertext from `kem_encapsulate` (1088 bytes = 2176
|
||||
hex chars). The decapsulated shared secret will match the encapsulating
|
||||
party's shared secret.
|
||||
|
||||
### 11.9 Example clients
|
||||
|
||||
See the `examples/` directory for working C clients demonstrating the new
|
||||
verbs:
|
||||
|
||||
- [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) — ML-DSA-65
|
||||
`get_public_key` + `sign_data`
|
||||
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768
|
||||
`get_public_key` + `kem_encapsulate` + `kem_decapsulate` (verifies shared
|
||||
secrets match)
|
||||
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519
|
||||
`get_public_key` + `ssh_sign`
|
||||
|
||||
---
|
||||
|
||||
## 12. Compatibility notes
|
||||
|
||||
- If you are writing an autonomous agent client, pin to explicit socket name and explicit role selector.
|
||||
- Keep method support feature-detected (`method_not_found` fallback).
|
||||
|
||||
@@ -61,13 +61,13 @@ Operational assumptions:
|
||||
Run `nsigner` in TCP listen mode:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[::]:8080
|
||||
./build/nsigner --listen tcp:[::]:11111
|
||||
```
|
||||
|
||||
Or bind to a specific FIPS ULA address:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[fd00::1234]:8080
|
||||
./build/nsigner --listen tcp:[fd00::1234]:11111
|
||||
```
|
||||
|
||||
Behavior notes:
|
||||
|
||||
+90
-4
@@ -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 **5–30 seconds**. This is a UX consideration, not a security issue. The hash-based signature scheme is intentionally compute-bound (that is its security foundation). On the Feather/CYD firmware, the approval prompt should show a "signing..." indicator during the operation.
|
||||
|
||||
The user should choose whether to use SLH-DSA-128s per role. For interactive use where latency matters, ML-DSA-65 is faster. SLH-DSA-128s is appropriate for low-frequency, high-assurance signing where minimal trust assumptions (hash-based, no number-theoretic hardness assumption) are desired.
|
||||
|
||||
### 16.5 Key sizes and memory
|
||||
|
||||
PQ private keys are large compared to classical keys:
|
||||
|
||||
| Algorithm | Private key | Public key | Signature / Ciphertext |
|
||||
|---|---|---|---|
|
||||
| secp256k1 | 32 bytes | 32 bytes | 64 bytes (sig) |
|
||||
| ed25519 | 32 bytes | 32 bytes | 64 bytes (sig) |
|
||||
| ML-DSA-65 | 4032 bytes | 1952 bytes | 3309 bytes (sig) |
|
||||
| SLH-DSA-128s | 64 bytes | 32 bytes | 7856 bytes (sig) |
|
||||
| ML-KEM-768 | 2400 bytes | 1184 bytes | 1088 bytes (ciphertext) |
|
||||
|
||||
With `ROLE_TABLE_MAX_ENTRIES` at 256, a full table of ML-DSA-65 keys would use ~1 MB of `mlock`'d memory (4032 × 256 ≈ 1.03 MB for private keys alone). This is acceptable on host. On ESP32 with 512 KB SRAM, this would not fit — on-demand derivation (deriving a PQ key only when a request targets that role) is the recommended pattern. The existing [`crypto_derive_one`](../src/key_store.c) path already supports this.
|
||||
|
||||
### 16.6 No hybrid signatures yet
|
||||
|
||||
Hybrid signatures (e.g., ed25519 + ML-DSA combined into one signature object) are **future work**. No standard exists for hybrid SSH signatures yet. `n_signer` provides the individual primitives (`sign_data` for ed25519, ML-DSA-65, and SLH-DSA-128s); a hybrid format can be assembled by the client once standards solidify.
|
||||
|
||||
### 16.7 PQ key persistence
|
||||
|
||||
**PQ keys are NOT persisted.** They are re-derived from the mnemonic on every startup, same as secp256k1. There is no PQ key file, no PQ key database, no PQ key cache on disk. Crash-equals-wipe (§10) applies unchanged: if the process dies, all PQ keys are gone and must be re-derived from the mnemonic on next startup.
|
||||
|
||||
This is a deliberate design choice. The deterministic derivation approach (§16.2) makes it possible to recover PQ keys from the mnemonic alone, so persistence would add risk (key material on disk) without adding capability.
|
||||
|
||||
---
|
||||
|
||||
## 17. References
|
||||
|
||||
- [`README.md`](../README.md) — authoritative behavior spec.
|
||||
- [`plans/nsigner.md`](../plans/nsigner.md) — root design plan and decisions log.
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# Derivation Paths — A Simple Explanation
|
||||
|
||||
## The seed
|
||||
|
||||
When you create a mnemonic (seed phrase), it generates a single master key. Think of it as the root of a tree — one key that controls everything below it.
|
||||
|
||||
## The tree
|
||||
|
||||
From that master key, you can derive **child keys**. Each child key can have its own children, and so on. This creates a tree of keys, all derived from the same seed.
|
||||
|
||||
## The path
|
||||
|
||||
A **derivation path** is just a set of directions for walking down the tree. It tells you which branches to take, starting from the master key (`m`).
|
||||
|
||||
```
|
||||
m / 44' / 1237' / 0' / 0 / 0
|
||||
```
|
||||
|
||||
Read it left to right:
|
||||
|
||||
| Segment | Meaning |
|
||||
|---------|---------|
|
||||
| `m` | The master key (your seed) |
|
||||
| `44'` | Purpose: "this is a BIP-44 wallet" |
|
||||
| `1237'` | Coin type: "this is Nostr" (1237 is Nostr's registered coin type) |
|
||||
| `0'` | Account: "account #0" |
|
||||
| `0` | Change: "external/receive" (0) vs "internal/change" (1) |
|
||||
| `0` | Address index: "address #0" |
|
||||
|
||||
Each segment derives a child key from the parent. Change any segment and you get a completely different key.
|
||||
|
||||
## The apostrophe (hardened vs unhardened)
|
||||
|
||||
The `'` after a number means **hardened**. It's the most important detail in the path.
|
||||
|
||||
### Without the apostrophe (unhardened)
|
||||
|
||||
```
|
||||
m / 44' / 1237' / 0' / 0 / 0
|
||||
^
|
||||
no apostrophe = unhardened
|
||||
```
|
||||
|
||||
Unhardened means: you can derive this child's **public key** from just the parent's **public key** — you don't need the private key.
|
||||
|
||||
This is useful for **watch-only wallets**: you can share the parent's extended public key with someone, and they can derive all the child public keys (addresses) without ever seeing your private key.
|
||||
|
||||
**The risk:** if a child **private key** leaks, and someone has the parent's extended public key, they can work backwards and derive **all sibling private keys**. So if address #5's private key leaks, addresses #0-4 and #6-99 are also compromised.
|
||||
|
||||
### With the apostrophe (hardened)
|
||||
|
||||
```
|
||||
m / 44' / 1237' / 0' / 0' / 0'
|
||||
^
|
||||
apostrophe = hardened
|
||||
```
|
||||
|
||||
Hardened means: you **need the parent's private key** to derive this child. You cannot derive it from the public key alone.
|
||||
|
||||
**The benefit:** if a child private key leaks, the attacker **cannot** derive sibling keys. Each hardened child is isolated. Compromising one doesn't compromise the others.
|
||||
|
||||
### Simple analogy
|
||||
|
||||
Imagine a building with floors and rooms:
|
||||
|
||||
- **Unhardened** = a glass door. Anyone with the floor key can see into all rooms on that floor. If someone picks the lock on room #5, they can figure out how to open rooms #0-4 and #6-99 too.
|
||||
- **Hardened** = a steel door. You need the master floor key to open any room. Picking the lock on room #5 tells you nothing about the other rooms.
|
||||
|
||||
### Visual: what happens when one key leaks
|
||||
|
||||
**Unhardened** (no apostrophe) — one leak compromises ALL siblings:
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0/0 ✓ safe
|
||||
m/44'/1237'/0'/0/1 ✓ safe
|
||||
m/44'/1237'/0'/0/2 ✓ safe
|
||||
m/44'/1237'/0'/0/3 ✓ safe
|
||||
m/44'/1237'/0'/0/4 ✓ safe
|
||||
m/44'/1237'/0'/0/5 ✗ COMPROMISED (leaked)
|
||||
m/44'/1237'/0'/0/6 ✗ COMPROMISED (derived from leak + parent pubkey)
|
||||
m/44'/1237'/0'/0/7 ✗ COMPROMISED (derived from leak + parent pubkey)
|
||||
...
|
||||
m/44'/1237'/0'/0/99 ✗ COMPROMISED (derived from leak + parent pubkey)
|
||||
|
||||
Parent extended public key (m/44'/1237'/0'/0) is public
|
||||
+ one child private key (address #5) leaks
|
||||
= ALL 100 sibling private keys are compromised
|
||||
```
|
||||
|
||||
**Hardened** (with apostrophe) — one leak only affects that one key:
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0'/0' ✓ safe
|
||||
m/44'/1237'/0'/0'/1' ✓ safe
|
||||
m/44'/1237'/0'/0'/2' ✓ safe
|
||||
m/44'/1237'/0'/0'/3' ✓ safe
|
||||
m/44'/1237'/0'/0'/4' ✓ safe
|
||||
m/44'/1237'/0'/0'/5' ✗ COMPROMISED (leaked)
|
||||
m/44'/1237'/0'/0'/6' ✓ safe (cannot be derived without parent PRIVATE key)
|
||||
m/44'/1237'/0'/0'/7' ✓ safe (cannot be derived without parent PRIVATE key)
|
||||
...
|
||||
m/44'/1237'/0'/0'/99' ✓ safe (cannot be derived without parent PRIVATE key)
|
||||
|
||||
One child private key (address #5) leaks
|
||||
= ONLY address #5 is compromised
|
||||
= siblings are safe because hardened derivation requires the parent PRIVATE key
|
||||
```
|
||||
|
||||
## Why NIP-06 uses unhardened last segments
|
||||
|
||||
NIP-06 (Nostr's key derivation standard) uses `m/44'/1237'/<account>'/0/0` — the first three segments are hardened, the last two are unhardened.
|
||||
|
||||
This is because NIP-06 copied the BIP-44 pattern from Bitcoin, where:
|
||||
- The **account** segment is hardened (so different accounts are isolated)
|
||||
- The **change** and **address** segments are unhardened (so watch-only wallets can derive addresses without the private key)
|
||||
|
||||
For Bitcoin, this makes sense: you want to share your extended public key with a payment processor so they can generate receive addresses for you.
|
||||
|
||||
For Nostr, it's less useful — but it means Nostr tools can derive your public keys from your extended public key, which some key management software uses.
|
||||
|
||||
## What this means for n_signer
|
||||
|
||||
n_signer always holds your private key and derives everything itself. You never share extended public keys with anyone. So:
|
||||
|
||||
- **Unhardened segments give you no benefit** — you don't need watch-only derivation
|
||||
- **Unhardened segments add risk** — the child key compromise vulnerability
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Harden everything** if you don't need NIP-06 compatibility:
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0'/0' ← all hardened, maximum isolation
|
||||
m/44'/1237'/0-99'/0'/0' ← all hardened, 100 isolated agent keys
|
||||
```
|
||||
|
||||
**Use NIP-06 paths** if you want compatibility with standard Nostr tools:
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0/0 ← NIP-06 standard (last two unhardened)
|
||||
m/44'/1237'/0-99'/0/0 ← NIP-06 compatible, 100 agent keys
|
||||
```
|
||||
|
||||
## Common path patterns
|
||||
|
||||
### Standard Nostr (NIP-06)
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0/0
|
||||
```
|
||||
|
||||
One key. The default Nostr key that tools like `nak keygen` produce.
|
||||
|
||||
### Multiple Nostr accounts (NIP-06)
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0/0 ← account 0
|
||||
m/44'/1237'/1'/0/0 ← account 1
|
||||
m/44'/1237'/2'/0/0 ← account 2
|
||||
```
|
||||
|
||||
Change the account segment (hardened) to get different Nostr identities.
|
||||
|
||||
### Multiple Nostr agents (hardened, maximum isolation)
|
||||
|
||||
```
|
||||
m/44'/1237'/0'/0'/0' ← agent 0
|
||||
m/44'/1237'/1'/0'/0' ← agent 1
|
||||
m/44'/1237'/2'/0'/0' ← agent 2
|
||||
```
|
||||
|
||||
Same as above but with the last two segments hardened. Each agent is fully isolated — compromising one doesn't compromise the others.
|
||||
|
||||
### Range and wildcard syntax (n_signer wizard)
|
||||
|
||||
In n_signer's role wizard, you can use range syntax or wildcard for the variable segment:
|
||||
|
||||
```
|
||||
m/44'/1237'/0-99'/0/0 ← agents 0-99, NIP-06 compatible
|
||||
m/44'/1237'/0-99'/0'/0' ← agents 0-99, all hardened
|
||||
m/44'/1237'/*'/0'/0' ← any agent index, all hardened (wildcard)
|
||||
```
|
||||
|
||||
- `0-99'` means "this segment can be any value from 0 to 99, hardened"
|
||||
- `*'` means "this segment can be any non-negative integer, hardened" (wildcard — no range limit)
|
||||
- `*` (without `'`) means "any non-negative integer, unhardened"
|
||||
|
||||
The client specifies the exact path (e.g. `m/44'/1237'/5'/0/0` for agent #5), and the server verifies it's within the role's allowed range (or accepts any value for `*`).
|
||||
|
||||
### SSH keys (ed25519)
|
||||
|
||||
```
|
||||
m/44'/102001'/0'/0'/0'
|
||||
```
|
||||
|
||||
SLIP-0010 derivation for ed25519. All segments are hardened (SLIP-0010 requires this for ed25519).
|
||||
|
||||
### Age / x25519 keys
|
||||
|
||||
```
|
||||
m/44'/102002'/0'/0'/0'
|
||||
```
|
||||
|
||||
Key agreement keys for Age encryption or X25519 ECDH.
|
||||
|
||||
### Post-quantum keys
|
||||
|
||||
```
|
||||
m/44'/102003'/0'/0'/0' ← ML-DSA-65 (signatures, FIPS 204)
|
||||
m/44'/102004'/0'/0'/0' ← SLH-DSA-128s (signatures, FIPS 205)
|
||||
m/44'/102005'/0'/0'/0' ← ML-KEM-768 (KEM, FIPS 203)
|
||||
```
|
||||
|
||||
All hardened. The mnemonic-derived seed feeds a SHAKE-256 DRBG that replaces PQClean's `randombytes()` during keygen.
|
||||
|
||||
## Summary
|
||||
|
||||
| Concept | Simple explanation |
|
||||
|---------|-------------------|
|
||||
| `m` | The master key (your seed) |
|
||||
| Numbers | Which branch to take at each level |
|
||||
| `'` (apostrophe) | "Hardened" — need private key to derive, isolates siblings |
|
||||
| No `'` | "Unhardened" — can derive from public key, but siblings can be compromised |
|
||||
| Path | A set of directions from the master key to a specific key |
|
||||
| Different path | Different key (always, no exceptions) |
|
||||
|
||||
**Golden rule:** If you don't need watch-only derivation (and n_signer doesn't), harden everything.
|
||||
@@ -0,0 +1,333 @@
|
||||
# nsigner Menu Reference
|
||||
|
||||
This document describes every interactive menu and screen in the `nsigner` TUI, in the order they appear during a session. Use this as the authoritative reference when discussing changes to the user experience.
|
||||
|
||||
## Startup sequence
|
||||
|
||||
The menus appear in this order during interactive (TUI) startup:
|
||||
|
||||
1. **Unlock — Mnemonic source**
|
||||
2. **Define a role — Role preset menu** (loops)
|
||||
3. **Transport — Transport selection**
|
||||
4. **Running phase — Main status display**
|
||||
|
||||
Non-interactive startup (`--mnemonic-stdin`, `--mnemonic-fd`, or piped input) skips menus 1–4 and creates a default `main` role automatically.
|
||||
|
||||
---
|
||||
|
||||
## 1. Unlock — Mnemonic source
|
||||
|
||||
**When:** First screen, before anything else.
|
||||
|
||||
**Title:** `> Unlock`
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
Mnemonic source: [E]nter existing or [G]enerate new
|
||||
Default is E; you can also paste full mnemonic here.
|
||||
>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Input | Action |
|
||||
|-------|--------|
|
||||
| `E` (default) | Prompt for an existing mnemonic (echo disabled) |
|
||||
| `G` | Generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning |
|
||||
| Paste full mnemonic | If the input contains spaces and doesn't start with `G`, it's treated as a mnemonic and validated directly |
|
||||
| `q` / `x` | Exit |
|
||||
|
||||
**After `E`:** Prompts for the mnemonic phrase with terminal echo disabled. Validates BIP-39 checksum. Up to 10 invalid attempts before exit.
|
||||
|
||||
**After `G`:** Displays the generated mnemonic numbered 1–12, then continues.
|
||||
|
||||
---
|
||||
|
||||
## 2. Define a role — Role preset menu
|
||||
|
||||
**When:** After mnemonic is loaded, in TUI mode only.
|
||||
|
||||
**Title:** `Define a role — bind a role name to a derivation path template`
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
Define a role:
|
||||
1. Standard Nostr (NIP-06): secp256k1, m/44'/1237'/0'/0/0
|
||||
2. Standard Nostr range: secp256k1, m/44'/1237'/*'/0/0
|
||||
3. Nostr agent range (hardened): secp256k1, m/44'/1237'/*'/1'/0'
|
||||
4. SSH role: ed25519, m/44'/102001'/0'/0'/0'
|
||||
5. Age/x25519 role: x25519, m/44'/102002'/0'/0'/0'
|
||||
6. ML-DSA-65 role: post-quantum signatures, m/44'/102003'/0'/0'/0'
|
||||
7. SLH-DSA-128s role: post-quantum signatures, m/44'/102004'/0'/0'/0'
|
||||
8. ML-KEM-768 role: post-quantum KEM, m/44'/102005'/0'/0'/0'
|
||||
9. OTP role (one-time pad encryption)
|
||||
10. Custom path
|
||||
Select [1]:
|
||||
```
|
||||
|
||||
**Preset defaults:**
|
||||
|
||||
| Choice | Default name | Default path | Curve | Purpose |
|
||||
|--------|-------------|-------------|-------|---------|
|
||||
| 1 | `main` | `m/44'/1237'/0'/0/0` | secp256k1 | nostr |
|
||||
| 2 | `nostr_range` | `m/44'/1237'/*'/0/0` | secp256k1 | nostr |
|
||||
| 3 | `nostr_agent` | `m/44'/1237'/*'/1'/0'` | secp256k1 | nostr |
|
||||
| 4 | `ssh` | `m/44'/102001'/0'/0'/0'` | ed25519 | ssh |
|
||||
| 5 | `age` | `m/44'/102002'/0'/0'/0'` | x25519 | age |
|
||||
| 6 | `ml_dsa_65` | `m/44'/102003'/0'/0'/0'` | ml-dsa-65 | pq_sig |
|
||||
| 7 | `slh_dsa_128s` | `m/44'/102004'/0'/0'/0'` | slh-dsa-128s | pq_sig |
|
||||
| 8 | `ml_kem_768` | `m/44'/102005'/0'/0'/0'` | ml-kem-768 | pq_kem |
|
||||
| 9 | `otp` | (pad file) | otp | n/a |
|
||||
| 10 | `custom` | `m/44'/1237'/0'/0/0` | (prompted) | (auto-detected) |
|
||||
|
||||
**After selecting a preset, the user is prompted for:**
|
||||
|
||||
### 2a. Role name
|
||||
```
|
||||
Role name [main]:
|
||||
```
|
||||
Editable line (arrow keys, backspace). Defaults to the preset's default name. If the name already exists, it's skipped.
|
||||
|
||||
### 2b. Curve (only for choice 10 — Custom)
|
||||
```
|
||||
Curve:
|
||||
1) secp256k1 (Nostr, Bitcoin)
|
||||
2) ed25519 (SSH)
|
||||
3) x25519 (key agreement, Age)
|
||||
4) ml-dsa-65 (post-quantum signatures)
|
||||
5) slh-dsa-128s (post-quantum signatures)
|
||||
6) ml-kem-768 (post-quantum KEM)
|
||||
Select [1]:
|
||||
```
|
||||
For presets 1–8, the curve is set automatically. For OTP (9), no curve is needed.
|
||||
|
||||
### 2c. Path template (only for choice 10 — Custom)
|
||||
```
|
||||
Path template [m/44'/1237'/0'/0/0]:
|
||||
|
||||
```
|
||||
Editable line. Pre-filled with the default path (updated to match the selected curve). Supports range syntax (`0-1000'`), set syntax (`1+34+54`), and wildcard (`*'` for any index).
|
||||
|
||||
For presets 1–8, the path is set automatically from the preset — no prompt. For OTP (9), no path is needed.
|
||||
|
||||
### 2c-otp. OTP pad file (only for choice 9 — OTP)
|
||||
```
|
||||
OTP pad directory (e.g. /media/usb0):
|
||||
OTP pad name (e.g. mypad):
|
||||
```
|
||||
Prompts for the pad directory and pad name. The pad is bound immediately. If binding fails, the role is skipped.
|
||||
|
||||
### 2d. Requires interactive approval
|
||||
```
|
||||
Require interactive approval for each request? [Y/n]:
|
||||
```
|
||||
- `Y` (default) → `requires_approval = 1` — human attendant must approve each request
|
||||
- `n` → `requires_approval = 0` — role name is the password, no prompt (role-as-password)
|
||||
|
||||
### 2e. Confirmation
|
||||
```
|
||||
Role 'main' registered: curve=secp256k1 path=m/44'/1237'/0'/0/0 (fixed, requires_approval=1).
|
||||
```
|
||||
Or for templated paths:
|
||||
```
|
||||
Role 'nostr_agent' registered: curve=secp256k1 path=m/44'/1237'/%d'/1'/0' (range 0-1000, requires_approval=1).
|
||||
```
|
||||
|
||||
### 2f. Loop
|
||||
```
|
||||
Define another role? [y/N]:
|
||||
```
|
||||
- `y` → back to the preset menu
|
||||
- `N` (default) → continue to transport selection
|
||||
|
||||
**Mandatory:** At least one role must be created. If the user exits without creating any roles, the signer prints "At least one role must be defined." and exits.
|
||||
|
||||
---
|
||||
|
||||
## 3. Transport — Transport selection
|
||||
|
||||
**When:** After role definition, in TUI mode with no `--listen` flag.
|
||||
|
||||
**Title:** `Transport — how should other programs reach this signer?`
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
Select one or more (type a number to toggle, 'a' for all, Enter to confirm):
|
||||
|
||||
[x] 1. Local Unix socket (same machine/qube)
|
||||
[ ] 2. Qubes qrexec bridge (other qubes via qrexec, no network)
|
||||
[ ] 3. FIPS/TCP listener (framed JSON, FIPS mesh or local network)
|
||||
[ ] 4. HTTP listener (curl-friendly, localhost by default)
|
||||
|
||||
[a] select all Enter = confirm
|
||||
>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Input | Action |
|
||||
|-------|--------|
|
||||
| `1` | Toggle Local Unix socket |
|
||||
| `2` | Toggle Qubes qrexec bridge |
|
||||
| `3` | Toggle FIPS/TCP listener |
|
||||
| `4` | Toggle HTTP listener |
|
||||
| `a` | Select all |
|
||||
| Enter | Confirm current selection (at least one required) |
|
||||
|
||||
Default: Unix socket only.
|
||||
|
||||
---
|
||||
|
||||
## 4. Running phase — Main status display
|
||||
|
||||
**When:** After all startup menus, this is the main screen.
|
||||
|
||||
**Title:** `> Main Menu`
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌ n_signer v<version> — Main Menu ─────────────────────────┐
|
||||
│ │
|
||||
│ Roles: │
|
||||
│ Role Purpose Curve Derivation path
|
||||
│ -------------------- ------------ ------------ ------------------------
|
||||
│ main nostr secp256k1 m/44'/1237'/0'/0/0
|
||||
│ role1 nostr secp256k1 m/44'/1237'/1-100'/0/0
|
||||
│ │
|
||||
│ Activity (latest first): │
|
||||
│ 14:51:05 uid:1000 nostr_get_public_key(pathrole_2) ALLOWED:prompt
|
||||
│ 14:50:54 uid:1000 nostr_get_public_key(pathrole_2) ALLOWED:prompt
|
||||
│ │
|
||||
│ session=unlocked (12 words) signer=nsigner derived=2 │
|
||||
│ │
|
||||
│ l lock/reunlock r refresh d display connections q/x quit
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
>
|
||||
```
|
||||
|
||||
**Status line:** `session=<locked|unlocked> (<N> words) signer=<name> derived=<count>`
|
||||
|
||||
**Menu items:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `l` | Lock / re-unlock (re-prompt for mnemonic) |
|
||||
| `r` | Refresh display |
|
||||
| `d` | Display connections (show transport details + example client commands) |
|
||||
| `q` / `x` | Quit |
|
||||
|
||||
**Note:** The `a toggle auto-approve` menu item has been **removed**. Authorization is now per-role via the `requires_approval` flag set during role definition.
|
||||
|
||||
---
|
||||
|
||||
## 5. Approval prompt
|
||||
|
||||
**When:** A client request arrives for a role with `requires_approval = 1`, and the request is not pre-approved by policy.
|
||||
|
||||
**Title:** `> Approval`
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌ n_signer v<version> — Approval ──────────────────────────┐
|
||||
│ │
|
||||
│ Approval required │
|
||||
│ caller: uid:1000 │
|
||||
│ method: nostr_sign_event │
|
||||
│ role: main │
|
||||
│ purpose: nostr │
|
||||
│ ** NEW IDENTITY — will be derived if approved ** │
|
||||
│ │
|
||||
│ y: allow once │
|
||||
│ n: deny │
|
||||
│ e: allow this caller+role+verb for session │
|
||||
│ a: allow this caller+role for session (all verbs) │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Key | Action | Policy result |
|
||||
|-----|--------|---------------|
|
||||
| `y` | Allow this one request | `POLICY_ALLOW` |
|
||||
| `n` | Deny this request | `POLICY_DENY` |
|
||||
| `e` | Allow this caller+role+verb for the rest of the session | `POLICY_ALLOW_SESSION_VERB` |
|
||||
| `a` | Allow this caller+role for all verbs for the session | `POLICY_ALLOW_SESSION_ALL` |
|
||||
|
||||
The `** NEW IDENTITY — will be derived if approved **` line appears only when the requested key hasn't been derived yet.
|
||||
|
||||
**Fields shown:**
|
||||
- `caller` — the caller identity (e.g. `uid:1000`, `qubes:vm-name`, `pubkey:<hex>`)
|
||||
- `fips peer` — (TCP/FIPS mode only) the peer's npub and optional name
|
||||
- `method` — the JSON-RPC verb (e.g. `nostr_sign_event`, `nostr_get_public_key`)
|
||||
- `role` — the role name from the request
|
||||
- `purpose` — the role's purpose (nostr, ssh, age, pq_sig, pq_kem)
|
||||
|
||||
---
|
||||
|
||||
## 6. Display connections
|
||||
|
||||
**When:** Pressed `d` from the main status display.
|
||||
|
||||
**Shows:** For each active transport, a section with:
|
||||
- Transport name and description
|
||||
- Socket address / port / URL
|
||||
- Example client command
|
||||
|
||||
After any keypress, returns to the main status display.
|
||||
|
||||
---
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
When started with `--mnemonic-stdin` or `--mnemonic-fd`, or when stdin is not a TTY:
|
||||
|
||||
- Menus 1–4 are skipped
|
||||
- A default `main` role is created automatically: `secp256k1`, `m/44'/1237'/0'/0/0`, `requires_approval=1`
|
||||
- The `--allow-all` flag sets `server_set_prompt_always_allow(1)` which bypasses approval prompts (used by tests and automated setups)
|
||||
- The `NSIGNER_TEST_NONINTERACTIVE_PROMPT` env var can be set to `allow` or `deny` to control the non-interactive prompt fallback
|
||||
|
||||
---
|
||||
|
||||
## CLI flags that affect menus
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--listen <mode>` | Skips transport selection menu (menu 3) |
|
||||
| `--mnemonic-stdin` | Skips mnemonic menu (menu 1), reads from stdin |
|
||||
| `--mnemonic-fd <N>` | Skips mnemonic menu (menu 1), reads from fd N |
|
||||
| `--allow-all` | Skips approval prompts (sets `prompt_always_allow`) |
|
||||
| `--socket-name <name>` | Sets the socket name (skips random name generation) |
|
||||
| `--preapprove <SPEC>` | Pre-approves specific caller+role+verb combinations |
|
||||
|
||||
---
|
||||
|
||||
## Summary flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> B{stdin is TTY?}
|
||||
B -- No --> C[Non-interactive: create default main role]
|
||||
B -- Yes --> D[Menu 1: Unlock — mnemonic source]
|
||||
D --> E[Menu 2: Define a role — preset menu]
|
||||
E --> F{Another role?}
|
||||
F -- Yes --> E
|
||||
F -- No --> G{At least one role?}
|
||||
G -- No --> H[Error: at least one role required]
|
||||
G -- Yes --> I{--listen flag?}
|
||||
I -- No --> J[Menu 3: Transport selection]
|
||||
I -- Yes --> K[Use --listen mode]
|
||||
J --> N[Menu 4: Main status display]
|
||||
K --> N
|
||||
C --> N
|
||||
N --> O{Request arrives}
|
||||
O --> P{requires_approval?}
|
||||
P -- No --> Q[Authorize immediately]
|
||||
P -- Yes --> R{Pre-approved?}
|
||||
R -- Yes --> Q
|
||||
R -- No --> S[Menu 6: Approval prompt]
|
||||
S -- y/e/a --> Q
|
||||
S -- n --> T[Deny]
|
||||
```
|
||||
@@ -140,11 +140,11 @@ def main() -> int:
|
||||
req = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "sign_event",
|
||||
"method": "nostr_sign_event",
|
||||
"params": params,
|
||||
}
|
||||
if not no_auth:
|
||||
req["auth"] = build_auth_envelope("sign_event", params, caller_priv)
|
||||
req["auth"] = build_auth_envelope("nostr_sign_event", params, caller_priv)
|
||||
|
||||
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(body)) + body
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer Feather WebUSB Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 980px;
|
||||
margin: 20px auto;
|
||||
padding: 0 14px 24px;
|
||||
}
|
||||
h1 { margin: 0 0 8px; font-size: 1.45rem; }
|
||||
p.note { margin: 0 0 14px; color: var(--muted); }
|
||||
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.86rem;
|
||||
color: var(--muted);
|
||||
display: block;
|
||||
margin: 6px 0 4px;
|
||||
}
|
||||
input, textarea, button {
|
||||
font: inherit;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
background: #0c131d;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
textarea { min-height: 84px; resize: vertical; }
|
||||
input[type="number"] { max-width: 130px; }
|
||||
|
||||
button {
|
||||
background: #1f6feb;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
}
|
||||
button[disabled] {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.secondary { background: #334155; }
|
||||
|
||||
.status {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #2a3648;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.status.ok { color: var(--good); border-color: #2f5a3a; }
|
||||
.status.err { color: var(--bad); border-color: #6a3131; }
|
||||
|
||||
pre {
|
||||
margin: 8px 0 0;
|
||||
background: #0a1018;
|
||||
border: 1px solid #1b2636;
|
||||
color: #d7e2ee;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
max-height: 220px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer Feather WebUSB Demo</h1>
|
||||
<p class="note">Connect to the board, choose a key index, fetch a pubkey, sign a kind 1 event, and test NIP-04 / NIP-44 encrypt + decrypt RPCs.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect WebUSB</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Public Key</h2>
|
||||
<label for="keyIndex">Key index (nostr_index)</label>
|
||||
<input id="keyIndex" type="text" inputmode="numeric" pattern="[0-9]*" value="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="pubkeyBtn" disabled>Get Public Key</button>
|
||||
</div>
|
||||
<pre id="pubkeyOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sign Kind 1 Event</h2>
|
||||
<label for="kind1Content">Content</label>
|
||||
<textarea id="kind1Content">hello from feather webusb demo</textarea>
|
||||
<label for="kind1Tags">Tags JSON (array)</label>
|
||||
<input id="kind1Tags" value="[]" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signKind1Btn" disabled>Create + Sign kind 1</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Encrypt</h2>
|
||||
<label for="nip04Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Msg">Plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>Encrypt (nip04_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Decrypt</h2>
|
||||
<label for="nip04DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Cipher">Ciphertext</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04DecBtn" disabled>Decrypt (nip04_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04DecOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Encrypt</h2>
|
||||
<label for="nip44Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Msg">Plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>Encrypt (nip44_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Decrypt</h2>
|
||||
<label for="nip44DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Cipher">Ciphertext</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44DecBtn" disabled>Decrypt (nip44_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44DecOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const pubkeyBtn = document.getElementById("pubkeyBtn");
|
||||
const signKind1Btn = document.getElementById("signKind1Btn");
|
||||
const nip04EncBtn = document.getElementById("nip04EncBtn");
|
||||
const nip04DecBtn = document.getElementById("nip04DecBtn");
|
||||
const nip44EncBtn = document.getElementById("nip44EncBtn");
|
||||
const nip44DecBtn = document.getElementById("nip44DecBtn");
|
||||
|
||||
const keyIndexEl = document.getElementById("keyIndex");
|
||||
const pubkeyOutEl = document.getElementById("pubkeyOut");
|
||||
|
||||
const kind1ContentEl = document.getElementById("kind1Content");
|
||||
const kind1TagsEl = document.getElementById("kind1Tags");
|
||||
const signOutEl = document.getElementById("signOut");
|
||||
|
||||
const nip04PeerEl = document.getElementById("nip04Peer");
|
||||
const nip04MsgEl = document.getElementById("nip04Msg");
|
||||
const nip04OutEl = document.getElementById("nip04Out");
|
||||
const nip04DecPeerEl = document.getElementById("nip04DecPeer");
|
||||
const nip04CipherEl = document.getElementById("nip04Cipher");
|
||||
const nip04DecOutEl = document.getElementById("nip04DecOut");
|
||||
|
||||
const nip44PeerEl = document.getElementById("nip44Peer");
|
||||
const nip44MsgEl = document.getElementById("nip44Msg");
|
||||
const nip44OutEl = document.getElementById("nip44Out");
|
||||
const nip44DecPeerEl = document.getElementById("nip44DecPeer");
|
||||
const nip44CipherEl = document.getElementById("nip44Cipher");
|
||||
const nip44DecOutEl = document.getElementById("nip44DecOut");
|
||||
|
||||
let dev = null;
|
||||
let iface = null;
|
||||
let ownPubkey = "";
|
||||
const EP_OUT = 1;
|
||||
const EP_IN = 1;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function utf8(s) {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
|
||||
function getIndexOptions() {
|
||||
const raw = Number.parseInt(String(keyIndexEl.value ?? "0"), 10);
|
||||
const index = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
||||
return { nostr_index: index };
|
||||
}
|
||||
|
||||
function pretty(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
// Demo caller key only (not secret in browser demo).
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
|
||||
const content = "webusb-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
|
||||
return {
|
||||
id,
|
||||
pubkey: callerPubX,
|
||||
created_at: createdAt,
|
||||
kind: 27235,
|
||||
tags,
|
||||
content,
|
||||
sig: sigHex,
|
||||
};
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await dev.transferOut(EP_OUT, frame);
|
||||
|
||||
const deadline = Date.now() + 10000;
|
||||
let ring = new Uint8Array(0);
|
||||
while (Date.now() < deadline) {
|
||||
const r = await dev.transferIn(EP_IN, 512);
|
||||
if (!r.data || r.data.byteLength === 0) continue;
|
||||
|
||||
const chunk = new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength);
|
||||
const next = new Uint8Array(ring.length + chunk.length);
|
||||
next.set(ring, 0);
|
||||
next.set(chunk, ring.length);
|
||||
ring = next;
|
||||
|
||||
while (ring.length >= 4) {
|
||||
const n = (ring[0] << 24) | (ring[1] << 16) | (ring[2] << 8) | ring[3];
|
||||
if (n <= 0 || n > 1_000_000) {
|
||||
ring = ring.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (ring.length < 4 + n) break;
|
||||
|
||||
const payload = ring.slice(4, 4 + n);
|
||||
ring = ring.slice(4 + n);
|
||||
const txt = new TextDecoder().decode(payload);
|
||||
return JSON.parse(txt);
|
||||
}
|
||||
}
|
||||
throw new Error("Timed out waiting for framed response");
|
||||
}
|
||||
|
||||
async function rpcCall(method, params, id = "web-1") {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { jsonrpc: "2.0", id, method, params, auth };
|
||||
log("→", method, JSON.stringify(params));
|
||||
const resp = await sendRpc(req);
|
||||
log("←", method, JSON.stringify(resp));
|
||||
return resp;
|
||||
}
|
||||
|
||||
function requirePeerHex(peer) {
|
||||
const v = String(peer || "").trim().toLowerCase();
|
||||
if (!/^[0-9a-f]{64}$/.test(v)) {
|
||||
throw new Error("Peer pubkey must be exactly 64 hex chars (x-only pubkey)");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function requireStringResult(resp, what) {
|
||||
if (resp && typeof resp.result === "string") {
|
||||
return resp.result;
|
||||
}
|
||||
throw new Error(`${what} failed: ${pretty(resp)}`);
|
||||
}
|
||||
|
||||
async function fetchOwnPubkeyAndFillPeers() {
|
||||
const params = [getIndexOptions()];
|
||||
const resp = await rpcCall("get_public_key", params, "web-own-pubkey");
|
||||
if (resp && typeof resp.result === "string") {
|
||||
ownPubkey = resp.result.trim().toLowerCase();
|
||||
pubkeyOutEl.textContent = pretty(resp);
|
||||
if (/^[0-9a-f]{64}$/.test(ownPubkey)) {
|
||||
nip04PeerEl.value = ownPubkey;
|
||||
nip04DecPeerEl.value = ownPubkey;
|
||||
nip44PeerEl.value = ownPubkey;
|
||||
nip44DecPeerEl.value = ownPubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
dev = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
|
||||
await dev.open();
|
||||
if (dev.configuration === null) {
|
||||
await dev.selectConfiguration(1);
|
||||
}
|
||||
|
||||
const intf = dev.configuration.interfaces.find(i =>
|
||||
i.alternates.some(a => a.interfaceClass === 0xff)
|
||||
);
|
||||
if (!intf) throw new Error("No vendor WebUSB interface found");
|
||||
|
||||
iface = intf.interfaceNumber;
|
||||
await dev.claimInterface(iface);
|
||||
const alt = intf.alternates.find(a => a.interfaceClass === 0xff);
|
||||
await dev.selectAlternateInterface(iface, alt.alternateSetting);
|
||||
|
||||
await dev.controlTransferOut({
|
||||
requestType: "class",
|
||||
recipient: "interface",
|
||||
request: 0x22,
|
||||
value: 1,
|
||||
index: iface,
|
||||
});
|
||||
|
||||
pubkeyBtn.disabled = false;
|
||||
signKind1Btn.disabled = false;
|
||||
nip04EncBtn.disabled = false;
|
||||
nip04DecBtn.disabled = false;
|
||||
nip44EncBtn.disabled = false;
|
||||
nip44DecBtn.disabled = false;
|
||||
setStatus(`Connected (iface ${iface})`, "ok");
|
||||
log("Connected. Interface", String(iface));
|
||||
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
log("Default peer pubkeys set to selected signer pubkey");
|
||||
} catch (e) {
|
||||
log("Auto pubkey fetch failed:", String(e));
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
setStatus("Connect failed", "err");
|
||||
log("Connect failed:", String(e));
|
||||
}
|
||||
});
|
||||
|
||||
pubkeyBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
} catch (e) {
|
||||
pubkeyOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
signKind1Btn.addEventListener("click", async () => {
|
||||
try {
|
||||
let tags = [];
|
||||
try {
|
||||
tags = JSON.parse(kind1TagsEl.value || "[]");
|
||||
if (!Array.isArray(tags)) throw new Error("tags must be an array");
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid tags JSON: ${String(e)}`);
|
||||
}
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: String(kind1ContentEl.value || ""),
|
||||
};
|
||||
|
||||
const params = [unsignedEvent, getIndexOptions()];
|
||||
const resp = await rpcCall("sign_event", params, "web-sign-kind1");
|
||||
signOutEl.textContent = pretty(resp?.result ?? resp);
|
||||
} catch (e) {
|
||||
signOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04PeerEl.value);
|
||||
const msg = String(nip04MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nip04_encrypt", params, "web-nip04-enc");
|
||||
nip04OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip04DecPeerEl.value = peer;
|
||||
nip04CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip04OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04DecPeerEl.value);
|
||||
const ciphertext = String(nip04CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nip04_decrypt", params, "web-nip04-dec");
|
||||
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
|
||||
} catch (e) {
|
||||
nip04DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44PeerEl.value);
|
||||
const msg = String(nip44MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nip44_encrypt", params, "web-nip44-enc");
|
||||
nip44OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip44DecPeerEl.value = peer;
|
||||
nip44CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip44OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44DecPeerEl.value);
|
||||
const ciphertext = String(nip44CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nip44_decrypt", params, "web-nip44-dec");
|
||||
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
|
||||
} catch (e) {
|
||||
nip44DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,7 @@ import time
|
||||
from coincurve import PrivateKey
|
||||
|
||||
HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips"
|
||||
PORT = 8080
|
||||
PORT = 11111
|
||||
|
||||
# Demo caller key (32 bytes). Replace with your stable caller key in real use.
|
||||
PRIVKEY = bytes(range(1, 33))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,14 +4,14 @@
|
||||
* bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_tcp <host> <port>
|
||||
* ./get_pubkey_tcp npub1xxx...fips 8080
|
||||
* ./get_pubkey_tcp npub1xxx...fips 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Output: for each index, prints:
|
||||
* index 0: hex=<64 hex chars> npub=npub1...
|
||||
@@ -95,7 +95,7 @@ static int query_pubkey(const char *host, int port, int nostr_index,
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed for index %d: %s\n", nostr_index,
|
||||
nsigner_client_last_error(client));
|
||||
params = NULL; /* nsigner_client_call took ownership even on failure */
|
||||
@@ -139,7 +139,7 @@ cleanup:
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *host = "127.0.0.1";
|
||||
int port = 8080;
|
||||
int port = 11111;
|
||||
char hex0[65], npub0[128];
|
||||
char hex1[65], npub1[128];
|
||||
int failures = 0;
|
||||
|
||||
@@ -59,7 +59,7 @@ int main(int argc, char **argv) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ def cmd_get_public_key(args):
|
||||
|
||||
def cmd_sign_event(args):
|
||||
event = json.loads(args.event)
|
||||
print(json.dumps(rpc(args, "sign_event", {"event": event}), indent=2))
|
||||
print(json.dumps(rpc(args, "nostr_sign_event", {"event": event}), indent=2))
|
||||
|
||||
|
||||
def build_parser():
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
* and bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* node n_signer_qube_example.js [host] [port]
|
||||
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 8080
|
||||
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Protocol:
|
||||
* - 4-byte big-endian length prefix + JSON payload (TCP framing)
|
||||
@@ -214,7 +214,7 @@ function hexToNpub(pubkeyHex) {
|
||||
|
||||
async function main() {
|
||||
const host = process.argv[2] || "127.0.0.1";
|
||||
const port = parseInt(process.argv[3] || "8080", 10);
|
||||
const port = parseInt(process.argv[3] || "11111", 10);
|
||||
|
||||
console.log(`Connecting to n_signer at ${host}:${port}`);
|
||||
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
otp_nostr_30078.py — example: encrypt data with OTP, wrap in a Nostr kind 30078
|
||||
event, sign it with n_signer, and print the signed event for publishing.
|
||||
|
||||
Workflow:
|
||||
1. Call n_signer's `otp_encrypt` verb to encrypt plaintext with the bound OTP pad.
|
||||
2. Build a Nostr kind 30078 (replaceable parameterized) event with the ASCII-armored
|
||||
ciphertext as the `content` field.
|
||||
3. Call n_signer's `sign_event` verb to sign the event with the secp256k1 key.
|
||||
4. Print the signed event JSON, ready to publish to Nostr relays.
|
||||
|
||||
This is a demo — it does not actually publish to a relay. To publish, send the
|
||||
signed event to your preferred Nostr relay using a library like nostr-tools,
|
||||
nostril, or nak.
|
||||
|
||||
Usage:
|
||||
python3 examples/otp_nostr_30078.py "Your secret message here"
|
||||
|
||||
Requirements:
|
||||
- n_signer running with --otp-pad-dir / --otp-pad bound, and a secp256k1
|
||||
role (e.g. "main") available for sign_event.
|
||||
- This script connects to n_signer via stdio (one process per request).
|
||||
|
||||
See plans/otp_nostr_integration.md for the full design.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
NSIGNER = "./build/nsigner"
|
||||
PAD_DIR = "/media/user/Music/pads"
|
||||
PAD_SPEC = "333e9902db839d9d"
|
||||
MNEMONIC_FILE = ".test_mnemonic"
|
||||
MNEMONIC_TMP = ".test_mnemonic_otp_30078.tmp"
|
||||
|
||||
|
||||
def send_framed(proc, obj):
|
||||
payload = json.dumps(obj).encode()
|
||||
proc.stdin.write(struct.pack(">I", len(payload)))
|
||||
proc.stdin.write(payload)
|
||||
proc.stdin.flush()
|
||||
|
||||
|
||||
def recv_framed(proc):
|
||||
"""Read a framed response, skipping any banner text on stdout."""
|
||||
buf = b""
|
||||
while True:
|
||||
b = proc.stdout.read(1)
|
||||
if not b:
|
||||
return None
|
||||
buf = (buf + b)[-4:]
|
||||
if len(buf) < 4:
|
||||
continue
|
||||
(length,) = struct.unpack(">I", buf)
|
||||
if 1 <= length <= 1024 * 1024:
|
||||
peek = proc.stdout.read(1)
|
||||
if peek == b"{":
|
||||
body = peek + proc.stdout.read(length - 1)
|
||||
return json.loads(body.decode())
|
||||
else:
|
||||
buf = (buf + peek)[-4:]
|
||||
|
||||
|
||||
def run_one_request(req_obj):
|
||||
"""Run nsigner in stdio mode for a single framed request/response."""
|
||||
shell_cmd = (
|
||||
f"exec 3<{MNEMONIC_TMP}; "
|
||||
f"exec {NSIGNER} --listen stdio --mnemonic-fd 3 "
|
||||
f"--otp-pad-dir {PAD_DIR} --otp-pad {PAD_SPEC} "
|
||||
f"--otp-allow-blkback --allow-all"
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
["bash", "-c", shell_cmd],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
import time as _time
|
||||
_time.sleep(1.0)
|
||||
if proc.poll() is not None:
|
||||
err = proc.stderr.read().decode()
|
||||
print(f"ERROR: nsigner exited early (code {proc.returncode})")
|
||||
print(f"stderr: {err}")
|
||||
return None
|
||||
send_framed(proc, req_obj)
|
||||
resp = recv_framed(proc)
|
||||
proc.stdin.close()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return resp
|
||||
|
||||
|
||||
def compute_event_id(event):
|
||||
"""Compute the Nostr event ID (SHA-256 of the canonical serialized event)."""
|
||||
# Nostr event serialization: [0, pubkey, created_at, kind, tags, content]
|
||||
serialized = json.dumps([
|
||||
0,
|
||||
event["pubkey"],
|
||||
event["created_at"],
|
||||
event["kind"],
|
||||
event["tags"],
|
||||
event["content"],
|
||||
], separators=(",", ":"), ensure_ascii=False)
|
||||
return hashlib.sha256(serialized.encode()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
plaintext = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Secret OTP message"
|
||||
print(f"Plaintext: {plaintext}")
|
||||
|
||||
# Prepare the mnemonic temp file.
|
||||
with open(MNEMONIC_FILE) as f:
|
||||
mnemonic = f.read().strip()
|
||||
with open(MNEMONIC_TMP, "w") as f:
|
||||
f.write(mnemonic + "\n")
|
||||
|
||||
try:
|
||||
# Step 1: Get the public key for the "main" role
|
||||
print("\n=== Step 1: get_public_key ===")
|
||||
resp = run_one_request({
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: get_public_key failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
# The result is a plain hex string for secp256k1 backward compat.
|
||||
pubkey_hex = resp["result"].strip('"')
|
||||
print(f"Public key: {pubkey_hex}")
|
||||
|
||||
# Step 2: Encrypt the plaintext with OTP
|
||||
print("\n=== Step 2: otp_encrypt ===")
|
||||
pt_b64 = base64.b64encode(plaintext.encode()).decode()
|
||||
resp = run_one_request({
|
||||
"id": "2",
|
||||
"method": "encrypt",
|
||||
"params": [pt_b64, {"algorithm": "otp", "encoding": "ascii"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: otp_encrypt failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
enc_result = json.loads(resp["result"])
|
||||
ciphertext = enc_result["ciphertext"]
|
||||
pad_chksum = enc_result["pad_chksum"]
|
||||
pad_offset = enc_result["pad_offset_after"]
|
||||
print(f"Pad checksum: {pad_chksum}")
|
||||
print(f"Pad offset after encrypt: {pad_offset}")
|
||||
print(f"Ciphertext (first 60 chars): {ciphertext[:60]}...")
|
||||
|
||||
# Step 3: Build the Nostr kind 30078 event
|
||||
print("\n=== Step 3: Build kind 30078 event ===")
|
||||
# Use a unique d-tag based on the pad checksum and offset.
|
||||
d_tag = f"otp-{pad_chksum[:16]}-{pad_offset}"
|
||||
event = {
|
||||
"pubkey": pubkey_hex,
|
||||
"created_at": int(time.time()),
|
||||
"kind": 30078,
|
||||
"tags": [
|
||||
["d", d_tag],
|
||||
["otp-pad", pad_chksum[:16]],
|
||||
["otp-version", "v0.0.2-otp"],
|
||||
["otp-encoding", "ascii"],
|
||||
],
|
||||
"content": ciphertext,
|
||||
}
|
||||
# Compute the event ID.
|
||||
event_id = compute_event_id(event)
|
||||
event["id"] = event_id
|
||||
print(f"Event ID: {event_id}")
|
||||
print(f"d-tag: {d_tag}")
|
||||
|
||||
# Step 4: Sign the event with n_signer
|
||||
print("\n=== Step 4: sign_event ===")
|
||||
# sign_event expects the event JSON as the first param (without id/sig).
|
||||
# The signer computes the id and signature internally.
|
||||
event_for_signing = {
|
||||
"pubkey": event["pubkey"],
|
||||
"created_at": event["created_at"],
|
||||
"kind": event["kind"],
|
||||
"tags": event["tags"],
|
||||
"content": event["content"],
|
||||
}
|
||||
resp = run_one_request({
|
||||
"id": "3",
|
||||
"method": "nostr_sign_event",
|
||||
"params": [json.dumps(event_for_signing), {"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: sign_event failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
sig = resp["result"].strip('"')
|
||||
event["sig"] = sig
|
||||
print(f"Signature: {sig[:60]}...")
|
||||
|
||||
# Step 5: Print the signed event
|
||||
print("\n=== Signed Nostr event (ready to publish) ===")
|
||||
print(json.dumps(event, indent=2))
|
||||
print(f"\nTo publish: send this event to a Nostr relay.")
|
||||
print(f"To decrypt: call otp_decrypt with the content field.")
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
os.unlink(MNEMONIC_TMP)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* pq_kem_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate post-quantum key encapsulation with ML-KEM-768.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an ML-KEM-768 role ("kem_main").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends a kem_encapsulate request with the public key, obtaining a
|
||||
* ciphertext + shared secret.
|
||||
* 4. Sends a kem_decapsulate request with the ciphertext, recovering the
|
||||
* shared secret on the signer side.
|
||||
* 5. Prints both shared secrets — they should match.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=pq-kem,
|
||||
* curve=ml-kem-768, named "kem_main" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./pq_kem_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: kem_main
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
char **out_pub_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_pub_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(parsed, "public_key");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
*out_pub_hex = strdup(pk_item->valuestring);
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* kem_encapsulate: returns ciphertext_hex and shared_secret_hex (newly
|
||||
* allocated, caller frees). */
|
||||
static int kem_encapsulate(nsigner_client_t *client, const char *role,
|
||||
const char *pub_hex,
|
||||
char **out_ct_hex, char **out_ss_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_ct_hex = NULL;
|
||||
*out_ss_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(pub_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "encapsulate", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *ct_item = cJSON_GetObjectItemCaseSensitive(parsed, "ciphertext");
|
||||
cJSON *ss_item = cJSON_GetObjectItemCaseSensitive(parsed, "shared_secret");
|
||||
if (cJSON_IsString(ct_item) && cJSON_IsString(ss_item)) {
|
||||
*out_ct_hex = strdup(ct_item->valuestring);
|
||||
*out_ss_hex = strdup(ss_item->valuestring);
|
||||
if (*out_ct_hex != NULL && *out_ss_hex != NULL) {
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *kem_decapsulate(nsigner_client_t *client, const char *role,
|
||||
const char *ct_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *ss_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(ct_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "decapsulate", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *ss_item = cJSON_GetObjectItemCaseSensitive(parsed, "shared_secret");
|
||||
if (cJSON_IsString(ss_item)) {
|
||||
ss_hex = strdup(ss_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return ss_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "kem_main";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
char *pub_hex = NULL;
|
||||
char *ct_hex = NULL;
|
||||
char *encap_ss_hex = NULL;
|
||||
char *decap_ss_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== PQ KEM Example (ML-KEM-768) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the ML-KEM-768 public key. */
|
||||
if (get_structured_pubkey(client, role, &pub_hex) != 0 || pub_hex == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Public Key:\n");
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pub_hex), strlen(pub_hex) / 2);
|
||||
printf(" pub_head: %.64s...\n", pub_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 2. Encapsulate with the public key. */
|
||||
printf("Encapsulating with public key...\n");
|
||||
if (kem_encapsulate(client, role, pub_hex, &ct_hex, &encap_ss_hex) != 0) {
|
||||
fprintf(stderr, "kem_encapsulate failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Ciphertext:\n");
|
||||
printf(" ct_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(ct_hex), strlen(ct_hex) / 2);
|
||||
printf(" ct_head: %.64s...\n", ct_hex);
|
||||
printf("Encapsulated shared secret:\n");
|
||||
printf(" ss: %s\n", encap_ss_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 3. Decapsulate with the ciphertext (uses the role's private key). */
|
||||
printf("Decapsulating ciphertext on signer side...\n");
|
||||
decap_ss_hex = kem_decapsulate(client, role, ct_hex);
|
||||
if (decap_ss_hex == NULL) {
|
||||
fprintf(stderr, "kem_decapsulate failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Decapsulated shared secret:\n");
|
||||
printf(" ss: %s\n", decap_ss_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 4. Verify the shared secrets match. */
|
||||
if (strcmp(encap_ss_hex, decap_ss_hex) == 0) {
|
||||
printf("SUCCESS: shared secrets match!\n");
|
||||
rc = 0;
|
||||
} else {
|
||||
printf("FAILURE: shared secrets do NOT match!\n");
|
||||
}
|
||||
|
||||
cleanup:
|
||||
free(pub_hex);
|
||||
free(ct_hex);
|
||||
free(encap_ss_hex);
|
||||
free(decap_ss_hex);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* pq_sign_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate post-quantum signing with ML-DSA-65.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an ML-DSA-65 role ("pq_sig").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends a sign_data request with a test message.
|
||||
* 4. Prints the signature (hex) and algorithm.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=pq-sig,
|
||||
* curve=ml-dsa-65, named "pq_sig" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./pq_sign_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: pq_sig
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
cJSON **out_obj) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_obj = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
/* result is a cJSON string containing the serialized structured object. */
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL && cJSON_IsObject(parsed)) {
|
||||
*out_obj = parsed;
|
||||
parsed = NULL;
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *sign_data(nsigner_client_t *client, const char *role,
|
||||
const char *msg_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *sig_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
/* result is a string containing {"signature":"<hex>","algorithm":"<alg>"} */
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *sig_item = cJSON_GetObjectItemCaseSensitive(parsed, "signature");
|
||||
if (cJSON_IsString(sig_item)) {
|
||||
sig_hex = strdup(sig_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return sig_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "pq_sig";
|
||||
/* "hello post-quantum world" in hex */
|
||||
const char *msg_hex = "68656c6c6f20706f73742d7175616e74756d20776f726c64";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *pubkey_obj = NULL;
|
||||
char *sig_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== PQ Sign Example (ML-DSA-65) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the structured public key. */
|
||||
if (get_structured_pubkey(client, role, &pubkey_obj) != 0 || pubkey_obj == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
cJSON *alg_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "algorithm");
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "public_key");
|
||||
cJSON *kid_item = cJSON_GetObjectItemCaseSensitive(pubkey_obj, "key_id");
|
||||
|
||||
printf("Public Key:\n");
|
||||
printf(" algorithm: %s\n",
|
||||
(cJSON_IsString(alg_item)) ? alg_item->valuestring : "?");
|
||||
printf(" key_id: %s\n",
|
||||
(cJSON_IsString(kid_item)) ? kid_item->valuestring : "?");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
/* ML-DSA-65 public key is 3904 hex chars — print length + prefix. */
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pk_item->valuestring), strlen(pk_item->valuestring) / 2);
|
||||
printf(" pub_head: %.64s...\n", pk_item->valuestring);
|
||||
} else {
|
||||
printf(" public_key: (missing)\n");
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
/* 2. Sign a test message. */
|
||||
printf("Signing message (hex): %s\n", msg_hex);
|
||||
sig_hex = sign_data(client, role, msg_hex);
|
||||
if (sig_hex == NULL) {
|
||||
fprintf(stderr, "sign_data failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("Signature:\n");
|
||||
printf(" sig_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(sig_hex), strlen(sig_hex) / 2);
|
||||
printf(" sig_head: %.64s...\n", sig_hex);
|
||||
printf("\n");
|
||||
|
||||
printf("To verify externally, use ML-DSA-65 (FIPS 204) verify with the\n");
|
||||
printf("public key above, the message, and this signature.\n");
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(sig_hex);
|
||||
cJSON_Delete(pubkey_obj);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ int main(int argc, char **argv) {
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL; /* owned by params now */
|
||||
|
||||
if (nsigner_client_call(client, "sign_event", params, &result) != NOSTR_SUCCESS) {
|
||||
if (nsigner_client_call(client, "nostr_sign_event", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* ssh_sign_example.c — connect to a running n_signer over its abstract UNIX
|
||||
* socket and demonstrate SSH signing with ed25519.
|
||||
*
|
||||
* The example:
|
||||
* 1. Sends a get_public_key request for an SSH/ed25519 role ("ssh_main").
|
||||
* 2. Prints the structured public key (algorithm, public_key, key_id).
|
||||
* 3. Sends an ssh_sign request with a test session ID.
|
||||
* 4. Prints the signature (hex) and algorithm.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - n_signer must be running with a role configured for purpose=ssh,
|
||||
* curve=ed25519, named "ssh_main" (or pass the role name as the 2nd arg).
|
||||
* - A mnemonic must be loaded in the signer.
|
||||
*
|
||||
* Usage: ./ssh_sign_example [socket_name] [role_name]
|
||||
*
|
||||
* Default socket_name: nsigner
|
||||
* Default role_name: ssh_main
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
static int get_structured_pubkey(nsigner_client_t *client, const char *role,
|
||||
char **out_pub_hex, char **out_key_id) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
int rc = -1;
|
||||
|
||||
*out_pub_hex = NULL;
|
||||
*out_key_id = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return -1;
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return -1;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(parsed, "public_key");
|
||||
cJSON *kid_item = cJSON_GetObjectItemCaseSensitive(parsed, "key_id");
|
||||
if (cJSON_IsString(pk_item)) {
|
||||
*out_pub_hex = strdup(pk_item->valuestring);
|
||||
}
|
||||
if (cJSON_IsString(kid_item)) {
|
||||
*out_key_id = strdup(kid_item->valuestring);
|
||||
}
|
||||
if (*out_pub_hex != NULL) {
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static char *ssh_sign(nsigner_client_t *client, const char *role,
|
||||
const char *msg_hex) {
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
char *sig_hex = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NULL;
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
params = NULL;
|
||||
|
||||
if (cJSON_IsString(result)) {
|
||||
cJSON *parsed = cJSON_Parse(result->valuestring);
|
||||
if (parsed != NULL) {
|
||||
cJSON *sig_item = cJSON_GetObjectItemCaseSensitive(parsed, "signature");
|
||||
if (cJSON_IsString(sig_item)) {
|
||||
sig_hex = strdup(sig_item->valuestring);
|
||||
}
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
return sig_hex;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *role = "ssh_main";
|
||||
/* A fake SSH session ID (32 bytes = 64 hex chars) for demonstration. */
|
||||
const char *session_id_hex =
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
char *pub_hex = NULL;
|
||||
char *key_id = NULL;
|
||||
char *sig_hex = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
if (argc > 2 && argv[2] != NULL && argv[2][0] != '\0') {
|
||||
role = argv[2];
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(socket_name, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL;
|
||||
|
||||
printf("=== SSH Sign Example (ed25519) ===\n");
|
||||
printf("socket: %s\n", socket_name);
|
||||
printf("role: %s\n", role);
|
||||
printf("\n");
|
||||
|
||||
/* 1. Get the ed25519 public key. */
|
||||
if (get_structured_pubkey(client, role, &pub_hex, &key_id) != 0 ||
|
||||
pub_hex == NULL) {
|
||||
fprintf(stderr, "get_public_key failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("Public Key:\n");
|
||||
printf(" algorithm: ed25519\n");
|
||||
printf(" key_id: %s\n", (key_id != NULL) ? key_id : "?");
|
||||
printf(" pub_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(pub_hex), strlen(pub_hex) / 2);
|
||||
printf(" pubkey: %s\n", pub_hex);
|
||||
printf("\n");
|
||||
|
||||
/* 2. Sign a test SSH session ID. */
|
||||
printf("Signing SSH session ID (hex): %s\n", session_id_hex);
|
||||
sig_hex = ssh_sign(client, role, session_id_hex);
|
||||
if (sig_hex == NULL) {
|
||||
fprintf(stderr, "ssh_sign failed: %s\n",
|
||||
nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("Signature:\n");
|
||||
printf(" sig_len: %zu hex chars (%zu bytes)\n",
|
||||
strlen(sig_hex), strlen(sig_hex) / 2);
|
||||
printf(" sig: %s\n", sig_hex);
|
||||
printf("\n");
|
||||
|
||||
printf("This ed25519 signature can be verified with the public key above\n");
|
||||
printf("using standard ed25519 verify (e.g. libsodium, OpenSSL EVP_DigestVerify).\n");
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(pub_hex);
|
||||
free(key_id);
|
||||
free(sig_hex);
|
||||
nsigner_client_free(client);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
+186
-3
@@ -46,13 +46,33 @@ CDC path:
|
||||
./.venv/bin/python ./examples/feather_sign_event.py /dev/ttyACM0
|
||||
```
|
||||
|
||||
WebUSB path:
|
||||
WebUSB / Web Serial path (unified):
|
||||
|
||||
- Open [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html)
|
||||
- Connect in Chrome/Edge
|
||||
- Open [`usb-test.html`](../usb-test.html) in Chrome/Edge
|
||||
- Connect via Web Serial (works for Teensy 4.1 USB CDC, CYD CH340, Feather, etc.)
|
||||
- Run `get_public_key`
|
||||
- Confirm pubkey matches CDC result
|
||||
|
||||
## CYD (ESP32-2432S028) validation — Web Serial
|
||||
|
||||
The CYD has no native USB; its CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`usb-test.html`](../usb-test.html):
|
||||
|
||||
- Open [`usb-test.html`](../usb-test.html) in Chrome/Edge
|
||||
- Click **Connect Web Serial**, select the CH340 port (`1a86:7523`)
|
||||
- Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp)
|
||||
- Each request shows the raw JSON-RPC request and response
|
||||
|
||||
The CYD firmware (v0.0.2+) speaks the same algorithm-based API as the host
|
||||
([`README.md`](../README.md) §4). The OTP pad is derived from the mnemonic
|
||||
seed (no USB pad on this board); the offset advances monotonically and is
|
||||
reported in every `encrypt`/`decrypt` response.
|
||||
|
||||
## Linux WebUSB host setup (one-time)
|
||||
|
||||
Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules:
|
||||
@@ -99,3 +119,166 @@ Notes:
|
||||
- Typical working range is ~4.7uF to 22uF; 10uF is recommended
|
||||
- Keep leads short for best stability
|
||||
- Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually
|
||||
|
||||
## Post-quantum crypto support (Phase 7)
|
||||
|
||||
Both firmware targets (`feather_s3_tft` and `cyd_esp32_2432s028`) now include
|
||||
the three NIST-standardized post-quantum algorithms alongside the existing
|
||||
secp256k1 (Nostr) and new ed25519/x25519 classical algorithms:
|
||||
|
||||
| Algorithm | Standard | Purpose | Pub key | Priv key | Sig/Ct |
|
||||
|---|---|---|---|---|---|
|
||||
| secp256k1 | — | Nostr (existing) | 32 B | 32 B | 64 B |
|
||||
| ed25519 | RFC 8032 | SSH signatures | 32 B | 32 B | 64 B |
|
||||
| x25519 | RFC 7748 | Key agreement (age) | 32 B | 32 B | — |
|
||||
| ML-DSA-65 | FIPS 204 | PQ signatures | 1952 B | 4032 B | 3309 B |
|
||||
| SLH-DSA-128s | FIPS 205 | PQ hash-based sigs | 32 B | 64 B | 7856 B |
|
||||
| ML-KEM-768 | FIPS 203 | PQ key encapsulation | 1184 B | 2400 B | 1088 B |
|
||||
|
||||
### mbedtls backend (vs OpenSSL on host)
|
||||
|
||||
The host build uses OpenSSL EVP for SHA-256, SHA-512, SHA3-256, SHA3-512,
|
||||
SHAKE-128, and SHAKE-256. On ESP32, OpenSSL is not available. Instead, the
|
||||
firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypto_backend.h`](../resources/pqclean/common/crypto_backend.h))
|
||||
with two implementations:
|
||||
|
||||
- [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP)
|
||||
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls + vendored Keccak)
|
||||
|
||||
The mbedtls backend uses:
|
||||
- `mbedtls_sha256()` for SHA-256 (ESP32 hardware accelerated where available)
|
||||
- `mbedtls_sha512()` for SHA-512 (ESP32 hardware accelerated where available)
|
||||
- A **self-contained Keccak-f[1600]** implementation (FIPS 202, public domain)
|
||||
for SHA3-256, SHA3-512, SHAKE-128, and SHAKE-256. This is vendored directly
|
||||
in `crypto_backend_mbedtls.c` because ESP-IDF v5.x mbedtls does not expose
|
||||
SHAKE (and SHA3 is only available when `CONFIG_MBEDTLS_SHA3_C` is set) through
|
||||
the `mbedtls_md` API. Carrying the Keccak core avoids any mbedtls config
|
||||
dependency for the PQ algorithms.
|
||||
|
||||
### ed25519 / x25519 via PSA crypto
|
||||
|
||||
ESP-IDF v5.x mbedtls removed the low-level `mbedtls_ed25519_*` functions. The
|
||||
firmware uses the **PSA Crypto API** for ed25519 sign/verify/key-derivation and
|
||||
x25519 key derivation + ECDH. Enable PSA in `sdkconfig.defaults`:
|
||||
|
||||
```
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
|
||||
```
|
||||
|
||||
### No SHA3/SHAKE menuconfig requirement
|
||||
|
||||
Because SHA3/SHAKE are provided by the vendored Keccak core (not mbedtls), you
|
||||
do **not** need to enable `CONFIG_MBEDTLS_SHA3_C` or any SHAKE config. The PQ
|
||||
algorithms build and run with the default mbedtls configuration.
|
||||
|
||||
### PQClean component
|
||||
|
||||
The PQClean algorithm code is compiled as an ESP-IDF component at
|
||||
`components/pqclean/`. The component's `CMakeLists.txt` references the shared
|
||||
source files in [`resources/pqclean/`](../resources/pqclean/) via relative
|
||||
paths, so there is a single source of truth for both host and firmware builds.
|
||||
|
||||
The component includes:
|
||||
- ML-DSA-65: `sign.c`, `poly.c`, `ntt.c`
|
||||
- SLH-DSA-128s: `sign.c`, `fors.c`, `wots.c`, `hash.c`, `thash.c`, `address.c`, `utils.c`
|
||||
- ML-KEM-768: `kem.c`, `indcpa.c`, `poly.c`, `ntt.c`, `cbd.c`, `reduce.c`, `symmetric.c`, `verify.c`
|
||||
- Common: `fips202.c`, `sha2.c`, `crypto_backend_mbedtls.c`
|
||||
- Firmware DRBG: `pq_drbg_firmware.c`, `randombytes_mbedtls.c`
|
||||
|
||||
### Flash usage estimates
|
||||
|
||||
| Algorithm | Code size (approx) |
|
||||
|---|---|
|
||||
| ML-DSA-65 | ~150 KB |
|
||||
| SLH-DSA-128s | ~80 KB |
|
||||
| ML-KEM-768 | ~120 KB |
|
||||
| Total PQ code | ~350 KB |
|
||||
|
||||
The ESP32-S3 (Feather S3 TFT) has 8 MB flash and the ESP32 (CYD) has 4 MB
|
||||
flash. The PQ code fits comfortably in both, but partition sizes may need
|
||||
adjustment if the total app image exceeds the default partition.
|
||||
|
||||
### RAM usage notes
|
||||
|
||||
PQ key buffers are large compared to classical ECC keys:
|
||||
|
||||
| Buffer | Size |
|
||||
|---|---|
|
||||
| ML-DSA-65 private key | 4032 bytes |
|
||||
| ML-DSA-65 public key | 1952 bytes |
|
||||
| ML-DSA-65 signature | 3309 bytes |
|
||||
| SLH-DSA-128s signature | 7856 bytes |
|
||||
| ML-KEM-768 private key | 2400 bytes |
|
||||
| ML-KEM-768 public key | 1184 bytes |
|
||||
| ML-KEM-768 ciphertext | 1088 bytes |
|
||||
|
||||
The ESP32 has ~320 KB available heap (after WiFi/BT are disabled). These
|
||||
buffers **must not be stack-allocated** — the default task stack is 8 KB.
|
||||
Use `malloc()` or static buffers. The firmware derives PQ keys **on demand**
|
||||
(not all at startup) to keep peak RAM usage low.
|
||||
|
||||
### SLH-DSA-128s signing latency warning
|
||||
|
||||
SLH-DSA-128s (SPHINCS+-128s) is a hash-based signature scheme with a deep
|
||||
hypertree structure (7 layers of WOTS+ + Merkle trees). On the ESP32-S3
|
||||
(240 MHz dual-core), expect:
|
||||
|
||||
- **Key generation**: 5–30 seconds
|
||||
- **Signing**: 5–30 seconds
|
||||
- **Verification**: 0.5–2 seconds
|
||||
|
||||
This is inherent to the algorithm — it trades computation for minimal trust
|
||||
assumptions (only SHA-256). The firmware logs a warning before SLH-DSA-128s
|
||||
keygen/signing. Users should choose whether to use SLH-DSA-128s per-role
|
||||
based on their latency tolerance. ML-DSA-65 is much faster (~100 ms for
|
||||
signing on ESP32-S3) and is the recommended PQ signature algorithm for
|
||||
interactive use.
|
||||
|
||||
### Derivation paths
|
||||
|
||||
All algorithms derive from the mnemonic using BIP-32/HMAC-SHA512 with
|
||||
SLIP-0010 all-hardened derivation for ed25519/x25519/PQ:
|
||||
|
||||
| Algorithm | Path | Notes |
|
||||
|---|---|---|
|
||||
| secp256k1 (Nostr) | `m/44'/1237'/<n>'/0/0` | NIP-06, existing |
|
||||
| ed25519 (SSH) | `m/44'/102001'/<n>'/0'/0'` | SLIP-0010 |
|
||||
| x25519 (age) | `m/44'/102002'/<n>'/0'/0'` | SLIP-0010 |
|
||||
| ML-DSA-65 | `m/44'/102003'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
| SLH-DSA-128s | `m/44'/102004'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
| ML-KEM-768 | `m/44'/102005'/<n>'/0'/0'` | seed → PQClean keygen |
|
||||
|
||||
The PQ derivation produces a 32-byte seed that feeds a deterministic
|
||||
SHAKE-256 DRBG ([`pq_drbg_firmware.c`](feather_s3_tft/components/pqclean/pq_drbg_firmware.c)),
|
||||
which replaces PQClean's `randombytes()` during keygen. This gives
|
||||
deterministic, mnemonic-recoverable PQ keys — same mnemonic, same key pair.
|
||||
|
||||
### Firmware API
|
||||
|
||||
The firmware exposes PQ operations via [`pq_crypto_firmware.h`](feather_s3_tft/main/pq_crypto_firmware.h):
|
||||
|
||||
```c
|
||||
/* Key generation (deterministic from mnemonic-derived seed) */
|
||||
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* Signing / verification */
|
||||
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen, ...);
|
||||
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen, ...);
|
||||
|
||||
/* KEM encaps / decaps */
|
||||
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
||||
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
||||
```
|
||||
|
||||
Key derivation from the mnemonic seed is via [`key_derivation.h`](feather_s3_tft/main/key_derivation.h):
|
||||
|
||||
```c
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index, ...);
|
||||
```
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# n_signer BLE Wearable Signer
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
A small, battery-powered wearable hardware signer that communicates with a host
|
||||
over **Bluetooth Low Energy (BLE)**. The host sends JSON-RPC requests over a
|
||||
BLE GATT characteristic; the signer shows an approval prompt on a tiny display;
|
||||
the user taps a button to approve; the signed response goes back over BLE.
|
||||
|
||||
## Concept
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: phone/laptop<br/>n_signer client] -->|BLE GATT| Signer[Wearable signer<br/>nRF52840 + OLED]
|
||||
Signer -->|approve/deny button| User[User]
|
||||
Signer -->|BLE GATT response| Host
|
||||
```
|
||||
|
||||
The signer speaks the same algorithm-based API as the host and the CYD/Teensy
|
||||
firmware ([`README.md`](../../README.md) §4). The auth envelope (kind 27235)
|
||||
protects the BLE wire — even if BLE is sniffed, an attacker can't forge
|
||||
requests without the caller's secp256k1 private key.
|
||||
|
||||
## Why BLE
|
||||
|
||||
- **Wearable form factor** — always with you (wristband, pendant, card)
|
||||
- **No physical connection** — no USB cable, no host-side driver, no dongle
|
||||
- **Universal host support** — phones, laptops, tablets all have BT
|
||||
- **Low power** — nRF52840 draws ~5 mA active, ~1 µA sleep
|
||||
|
||||
## Hardware (preliminary)
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **nRF52840** (Nordic) | Cortex-M4 @ 64 MHz, 1 MB flash, 256 KB RAM, BT 5.0, hardware AES/ECC, USB device, NFC-A. ~$5-8. |
|
||||
| Display | 0.96" or 1.3" SSD1306 OLED (I2C) or 1.02" e-paper | Small is fine — only shows "approve kind 1 from <caller>?" |
|
||||
| Input | 2-3 tactile buttons (approve/deny/back) | No touch at this size |
|
||||
| Power | 200 mAh coin cell or small LiPo | Weeks of battery life |
|
||||
| Mnemonic entry | Buttons (scroll words), NFC from phone, or generate-on-device | The hard UX problem |
|
||||
|
||||
## Security considerations
|
||||
|
||||
- **BT stack attack surface:** BLE has a large stack (pairing, GATT, L2CAP, SMP).
|
||||
A stack bug could allow code execution. Mitigations: use Nordic's audited
|
||||
SoftDevice, disable unnecessary services, require LE Secure Connections pairing.
|
||||
- **Radio range (~10 m):** an attacker in the same room could potentially
|
||||
interact with the signer. The auth envelope + approval prompt protect against
|
||||
this, but the radio is omnidirectional.
|
||||
- **Pairing UX:** BT pairing can be frustrating. LE Secure Connections (Numeric
|
||||
Comparison) is the most secure and user-friendly pairing method.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Mnemonic entry on a tiny screen:** scroll through 2048 BIP-39 words with
|
||||
up/down buttons (like Coldcard)? Load via NFC from a phone? Generate on-device
|
||||
and display for the user to write down?
|
||||
- **PQ crypto on nRF52840:** 256 KB RAM is enough for ML-DSA-65 (~6 KB heap)
|
||||
but SLH-DSA-128s is heavy. May need to limit the PQ algorithm set or stream
|
||||
the keygen.
|
||||
- **Display choice:** OLED (fast refresh, high power) vs e-paper (slow refresh,
|
||||
zero power when static, persistent display).
|
||||
- **Form factor:** wristband? pendant? card? What's the target use case —
|
||||
daily signing, emergency key access, or a backup signer?
|
||||
|
||||
## Comparison to the IR air-gap signer
|
||||
|
||||
| | BLE wearable | IR air-gap |
|
||||
|---|---|---|
|
||||
| Air-gap | Medium (radio, ~10 m, omnidirectional) | High (light, line-of-sight, ~1 m) |
|
||||
| Attack surface | Large (BT stack) | Small (no BT, dumb dongle) |
|
||||
| Host compatibility | Universal (phones, laptops) | Requires USB dongle |
|
||||
| Form factor | Wearable | Handheld (point at dongle) |
|
||||
| Throughput | ~250 KB/s (BLE 5) | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) |
|
||||
| Novelty | Conventional | Novel (no hardware wallet uses IR) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide on the MCU (nRF52840 vs RP2040+BT-module)
|
||||
- Decide on mnemonic entry method
|
||||
- Decide on display (OLED vs e-paper)
|
||||
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
@@ -0,0 +1,175 @@
|
||||
# n_signer CYD Firmware (ESP32-2432S028)
|
||||
|
||||
The **Cheap Yellow Display** (ESP32-2432S028) is a $15 ESP32-WROOM-32 board with
|
||||
a 2.8" 320×240 ILI9341 resistive-touch display, CH340 USB-UART bridge, and a
|
||||
Micro SD card slot. This firmware turns it into a hardware n_signer that speaks
|
||||
the same algorithm-based API as the host ([`README.md`](../../README.md) §4).
|
||||
|
||||
**Firmware version:** 0.0.2 (algorithm-based API)
|
||||
|
||||
## Hardware summary
|
||||
|
||||
| Concern | Value |
|
||||
|---|---|
|
||||
| MCU | ESP32-WROOM-32 (classic, dual-core Xtensa, 512 KB SRAM, no PSRAM) |
|
||||
| USB-UART | CH340 (`1a86:7523`) → `/dev/ttyUSB0` |
|
||||
| Flash | 4 MB |
|
||||
| Display | 2.8" 320×240 ILI9341 (HSPI: DC=IO2, CS=IO15, SCK=IO14, MOSI=IO13, MISO=IO12, BL=IO21) |
|
||||
| Touch | XPT2046 resistive (bit-banged SPI: CLK=IO25, MOSI=IO32, CS=IO33, MISO=IO39, IRQ=IO36) |
|
||||
| SD card | Micro SD, VSPI (CS=IO5, SCK=IO18, MISO=IO19, MOSI=IO23) |
|
||||
| RGB LED | R=IO4, G=IO16, B=IO17 (active LOW) |
|
||||
| LDR | IO34 |
|
||||
| Speaker | IO26 (DAC) |
|
||||
| BOOT button | IO0 |
|
||||
| GUI | LVGL 8.3 |
|
||||
|
||||
For the full pin map, connectors (P1/P3/CN1), and add-ons, see the upstream
|
||||
hardware docs copied to [`docs/`](docs/) — especially
|
||||
[`docs/PINS.md`](docs/PINS.md) and [`docs/SETUP.md`](docs/SETUP.md).
|
||||
|
||||
## SD card — size limits and OTP pad storage
|
||||
|
||||
The CYD's Micro SD slot is wired to VSPI (IO5/18/19/23). ESP-IDF drives it via
|
||||
the SDSPI host + FATFS filesystem. The proven bring-up example is
|
||||
`07_sd_card` in the `esp32_playground/cyb-esp32-2432s028/` workspace.
|
||||
|
||||
**Size limits:**
|
||||
|
||||
- **SDSC (≤ 2 GB):** supported.
|
||||
- **SDHC (2 GB – 32 GB):** supported — this is the recommended range. The
|
||||
Makerfabs CYD ships with a 16 GB card, which works.
|
||||
- **SDXC (> 32 GB):** **not supported** out of the box. SDXC cards ship
|
||||
formatted as exFAT, and ESP-IDF's FATFS does not include exFAT. An SDXC card
|
||||
reformatted to FAT32 will work up to 32 GB; beyond that, FAT32's 32 GB limit
|
||||
applies. For OTP pad storage, 32 GB is vastly more than enough (see below).
|
||||
|
||||
**Recommendation:** use any **SDHC card from 4–32 GB** formatted **FAT32**.
|
||||
|
||||
### Using the SD card for the OTP pad
|
||||
|
||||
The current v0.0.2 firmware derives the OTP pad from the mnemonic seed via
|
||||
HKDF-SHA256 into a 1024-byte in-RAM pad (no SD card required). This keeps the
|
||||
wire contract identical to the host's `encrypt`/`decrypt` (algorithm:"otp")
|
||||
verbs but limits the pad to 1024 bytes per session.
|
||||
|
||||
To hold a **large OTP pad** (the original n_signer host design binds a pad file
|
||||
from `--otp-pad-dir`), the SD card is the right storage. The plan:
|
||||
|
||||
1. Format the SD card as FAT32.
|
||||
2. Place a pad file (e.g. `nsigner.pad`) on it — any size up to the card's free
|
||||
space. A 1 GB pad gives ~1 billion one-time-pad bytes before exhaustion.
|
||||
3. The firmware mounts the SD card at boot via `esp_vfs_fat_sdmmc_mount()` on
|
||||
the SDSPI host, opens the pad file, and reads pad bytes on demand into a
|
||||
small ring buffer, advancing a persistent offset (stored in a small
|
||||
`nsigner.offset` file on the SD so the offset survives power cycles).
|
||||
4. The `encrypt`/`decrypt` verbs XOR against the SD-backed pad instead of the
|
||||
HKDF-derived in-RAM pad.
|
||||
|
||||
This is a planned enhancement (see [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md)
|
||||
§13 — the current implementation uses the mnemonic-derived pad as the embedded
|
||||
fallback). The SD card slot is confirmed working and the pin map is in
|
||||
[`docs/PINS.md`](docs/PINS.md).
|
||||
|
||||
**Note on simultaneous display + touch + SD:** The CYD's display (HSPI), touch
|
||||
(bit-banged), and SD (VSPI) use three different SPI buses. All three can run at
|
||||
the same time — the touch is bit-banged precisely so it doesn't contend with
|
||||
the other two hardware SPI buses (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)).
|
||||
|
||||
## Building and flashing
|
||||
|
||||
Requires ESP-IDF v5.x (tested with v5.4.2). The classic ESP32 target uses the
|
||||
`xtensa-esp-elf` unified toolchain.
|
||||
|
||||
```bash
|
||||
source /home/user/esp/esp-idf/export.sh
|
||||
cd firmware/cyd_esp32_2432s028
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash
|
||||
```
|
||||
|
||||
If flashing fails with `Wrong boot mode detected (0x13)`, see the serial-reset
|
||||
hardware note below.
|
||||
|
||||
## Validation — Web Serial
|
||||
|
||||
The CYD has no native USB; the CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`usb-test.html`](../../usb-test.html):
|
||||
|
||||
1. Open [`usb-test.html`](../../usb-test.html) in Chrome/Edge.
|
||||
2. Click **Connect Web Serial**, select the CH340 port (`1a86:7523`).
|
||||
3. On the CYD touchscreen, enter or generate a mnemonic to reach the "ready" state.
|
||||
4. Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp).
|
||||
|
||||
## API
|
||||
|
||||
The CYD firmware speaks the same algorithm-based API as the host n_signer
|
||||
([`README.md`](../../README.md) §4). Supported verbs:
|
||||
|
||||
| Verb | Algorithms |
|
||||
|---|---|
|
||||
| `get_public_key` | secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768 |
|
||||
| `sign` / `verify` | secp256k1 (schnorr/ecdsa), ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 (HMAC-SHA256) |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06, single-threaded PoW) |
|
||||
| `nostr_nip04_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
| `nostr_nip44_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
|
||||
All requests require an auth envelope (kind 27235). The `key_id` in every
|
||||
structured result is the first 16 hex characters of the public key, matching
|
||||
the host. Invalid `(verb, algorithm)` pairs return error `1010`.
|
||||
|
||||
### Embedded-specific notes
|
||||
|
||||
- **OTP pad:** derived from the mnemonic seed (HKDF-SHA256, 1024 bytes) in
|
||||
v0.0.2. The offset advances monotonically and is reported in every
|
||||
`encrypt`/`decrypt` response. SD-card-backed pad is a planned enhancement
|
||||
(see above).
|
||||
- **`nostr_mine_event`:** single-threaded, hard 30 s default timeout, shows a
|
||||
"mining…" screen. Keep difficulty low (≤ 8) on ESP32.
|
||||
- **SLH-DSA-128s:** keygen and signing take 5–30 s. The UI shows a "deriving
|
||||
key…" / "signing…" indicator. ML-DSA-65 is much faster (~100 ms) and is the
|
||||
recommended PQ signature algorithm for interactive use.
|
||||
|
||||
## Crypto backend
|
||||
|
||||
- **SHA-256 / SHA-512:** mbedtls (ESP32 hardware accelerated).
|
||||
- **SHA3 / SHAKE-128 / SHAKE-256:** vendored Keccak-f[1600] (FIPS 202) in
|
||||
[`resources/pqclean/common/crypto_backend_mbedtls.c`](../../resources/pqclean/common/crypto_backend_mbedtls.c).
|
||||
No `CONFIG_MBEDTLS_SHA3_C` or SHAKE menuconfig dependency.
|
||||
- **ed25519 / x25519:** PSA Crypto API (`psa_import_key`, `psa_sign_message`,
|
||||
`psa_raw_key_agreement`, etc.) — ESP-IDF v5.x mbedtls removed the
|
||||
`mbedtls_ed25519_*` functions. Requires `CONFIG_MBEDTLS_PSA_CRYPTO_C=y`
|
||||
(set in [`sdkconfig.defaults`](sdkconfig.defaults)).
|
||||
- **secp256k1:** the vendored secp256k1 component (schnorr + ECDSA).
|
||||
- **PQ (ML-DSA-65, SLH-DSA-128s, ML-KEM-768):** PQClean via the
|
||||
[`components/pqclean/`](components/pqclean/) component.
|
||||
|
||||
## Serial-reset hardware note (CH340 auto-reset)
|
||||
|
||||
Opening `/dev/ttyUSB0` can reset the ESP32 because the CH340's DTR/RTS lines
|
||||
are wired into the ESP32 auto-reset circuit. Symptoms: the device returns to
|
||||
the startup menu when a host app opens the serial port.
|
||||
|
||||
**Mitigation:** add a **10 µF capacitor between EN and GND** on the CYD board
|
||||
(negative leg to GND). Typical working range is 4.7–22 µF. This also fixes the
|
||||
`Wrong boot mode detected (0x13)` flashing error. See
|
||||
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) and the
|
||||
[`firmware/README.md`](../README.md) CYD section for details.
|
||||
|
||||
## Reference documentation
|
||||
|
||||
- [`docs/`](docs/) — upstream CYD hardware docs (PINS, SETUP, TROUBLESHOOTING, ADDONS, etc.)
|
||||
- [`plans/cyd_signer_port.md`](../../plans/cyd_signer_port.md) — original port plan (hardware comparison, architecture, UI flow)
|
||||
- [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md) — v0.0.2 API upgrade plan
|
||||
- [`firmware/README.md`](../README.md) — shared firmware README (PQ crypto, mbedtls backend, feather target)
|
||||
- [`README.md`](../../README.md) §4 — the authoritative n_signer API reference
|
||||
@@ -0,0 +1,60 @@
|
||||
# CMakeLists.txt — ESP-IDF component for PQClean post-quantum algorithms.
|
||||
#
|
||||
# Compiles the three PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
|
||||
# from the shared resources/pqclean/ source tree, using the mbedtls
|
||||
# crypto backend (crypto_backend_mbedtls.c) for SHA-2/SHA3/SHAKE.
|
||||
#
|
||||
# The source files are referenced via relative paths back to the shared
|
||||
# resources/pqclean/ directory so there is a single source of truth.
|
||||
#
|
||||
# mbedtls requirements:
|
||||
# CONFIG_MBEDTLS_SHA3_C=y (for SHA3-256, SHA3-512)
|
||||
# CONFIG_MBEDTLS_SHAKE_C=y (for SHAKE-128, SHAKE-256)
|
||||
# Enable these in menuconfig under Component config -> mbedTLS ->
|
||||
# Hash functions -> SHA-3 and SHAKE.
|
||||
|
||||
set(PQCLEAN_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../resources/pqclean")
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/fors.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/wots.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/hash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/thash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/address.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/utils.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/kem.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/indcpa.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/cbd.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/reduce.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/symmetric.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/verify.c"
|
||||
"${PQCLEAN_ROOT}/common/fips202.c"
|
||||
"${PQCLEAN_ROOT}/common/sha2.c"
|
||||
"${PQCLEAN_ROOT}/common/crypto_backend_mbedtls.c"
|
||||
"randombytes_mbedtls.c"
|
||||
"pq_drbg_firmware.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
"${PQCLEAN_ROOT}/common"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768"
|
||||
REQUIRES
|
||||
mbedtls
|
||||
)
|
||||
|
||||
# Suppress warnings from the PQClean code (it uses C99 patterns that
|
||||
# trigger -Wextra warnings under ESP-IDF's default flags).
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
-Wno-unused-parameter
|
||||
-Wno-sign-compare
|
||||
-Wno-unused-variable
|
||||
-Wno-unused-but-set-variable
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_dsa_65_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/ml-dsa-65/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_kem_768_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_kem/ml-kem-768/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
/* pqclean.h — Umbrella include for the ESP32 firmware PQClean component.
|
||||
*
|
||||
* Exposes the three post-quantum algorithms (ML-DSA-65, SLH-DSA-128s,
|
||||
* ML-KEM-768) and the deterministic DRBG used for mnemonic-recoverable
|
||||
* key generation.
|
||||
*
|
||||
* On ESP32 the underlying hash/SHAKE primitives are provided by the
|
||||
* mbedtls backend (crypto_backend_mbedtls.c) instead of OpenSSL.
|
||||
*/
|
||||
#ifndef FIRMWARE_PQCLEAN_H
|
||||
#define FIRMWARE_PQCLEAN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- ML-DSA-65 (FIPS 204, lattice signatures) --- */
|
||||
#include "ml_dsa_65_api.h"
|
||||
|
||||
/* --- SLH-DSA-128s (FIPS 205, hash-based signatures) --- */
|
||||
#include "slh_dsa_128s_api.h"
|
||||
|
||||
/* --- ML-KEM-768 (FIPS 203, lattice KEM) --- */
|
||||
#include "ml_kem_768_api.h"
|
||||
|
||||
/* --- Deterministic DRBG (replaces randombytes() for keygen) --- */
|
||||
/* Initializes the DRBG with a 32-byte mnemonic-derived seed. Subsequent
|
||||
* randombytes() calls will produce a deterministic byte stream. */
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
|
||||
|
||||
/* Zeroizes the DRBG state (call after keygen to wipe sensitive material). */
|
||||
void pq_drbg_zeroize(void);
|
||||
|
||||
/* randombytes() — called by the PQClean algorithm code.
|
||||
* On firmware this is provided by randombytes_mbedtls.c (deterministic DRBG
|
||||
* for keygen, or mbedtls_ctr_drbg for real randomness during encaps). */
|
||||
int randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
#endif /* FIRMWARE_PQCLEAN_H */
|
||||
@@ -0,0 +1,5 @@
|
||||
/* slh_dsa_128s_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#define FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/slh-dsa-128s/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
/* pq_drbg_firmware.c — Deterministic PRNG for PQ key generation on ESP32.
|
||||
*
|
||||
* Same algorithm as the host's src/pq_drbg.c but uses the crypto backend
|
||||
* abstraction (which resolves to mbedtls on ESP32) for SHAKE-256 instead
|
||||
* of OpenSSL EVP. This allows deterministic PQ key generation from a
|
||||
* mnemonic-derived seed: same seed -> same randombytes output sequence.
|
||||
*
|
||||
* The PRNG: SHAKE-256(seed || counter) produces a stream of pseudo-random
|
||||
* bytes. The counter is a 64-bit little-endian integer that increments
|
||||
* each time we need more output.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "crypto_backend.h"
|
||||
|
||||
/* --- DRBG state --- */
|
||||
|
||||
static unsigned char g_seed[32];
|
||||
static int g_seed_len = 0;
|
||||
static uint64_t g_counter = 0;
|
||||
static unsigned char g_buffer[168]; /* SHAKE-256 rate = 136, 168 for safety */
|
||||
static size_t g_buffer_pos = sizeof(g_buffer);
|
||||
static int g_initialized = 0;
|
||||
|
||||
/* --- internal: squeeze more bytes from SHAKE-256 --- */
|
||||
|
||||
static void drbg_refill(void) {
|
||||
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
|
||||
|
||||
memcpy(seed_block, g_seed, (size_t)g_seed_len);
|
||||
seed_block[g_seed_len + 0] = (unsigned char)(g_counter & 0xFF);
|
||||
seed_block[g_seed_len + 1] = (unsigned char)((g_counter >> 8) & 0xFF);
|
||||
seed_block[g_seed_len + 2] = (unsigned char)((g_counter >> 16) & 0xFF);
|
||||
seed_block[g_seed_len + 3] = (unsigned char)((g_counter >> 24) & 0xFF);
|
||||
seed_block[g_seed_len + 4] = (unsigned char)((g_counter >> 32) & 0xFF);
|
||||
seed_block[g_seed_len + 5] = (unsigned char)((g_counter >> 40) & 0xFF);
|
||||
seed_block[g_seed_len + 6] = (unsigned char)((g_counter >> 48) & 0xFF);
|
||||
seed_block[g_seed_len + 7] = (unsigned char)((g_counter >> 56) & 0xFF);
|
||||
|
||||
crypto_backend_shake256(seed_block, (size_t)g_seed_len + 8,
|
||||
g_buffer, sizeof(g_buffer));
|
||||
|
||||
g_counter++;
|
||||
g_buffer_pos = 0;
|
||||
}
|
||||
|
||||
/* --- public API --- */
|
||||
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len) {
|
||||
if (seed == NULL || seed_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_seed, 0, sizeof(g_seed));
|
||||
if (seed_len > sizeof(g_seed)) {
|
||||
seed_len = sizeof(g_seed);
|
||||
}
|
||||
memcpy(g_seed, seed, seed_len);
|
||||
g_seed_len = (int)sizeof(g_seed); /* always use 32-byte seed (zero-padded) */
|
||||
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 1;
|
||||
}
|
||||
|
||||
void pq_drbg_zeroize(void) {
|
||||
crypto_backend_cleanse(g_seed, sizeof(g_seed));
|
||||
crypto_backend_cleanse(g_buffer, sizeof(g_buffer));
|
||||
g_seed_len = 0;
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 0;
|
||||
}
|
||||
|
||||
/* Returns 1 if the DRBG has been initialized (keygen mode), 0 otherwise.
|
||||
* Used by randombytes_mbedtls.c to decide between deterministic DRBG and
|
||||
* hardware RNG. */
|
||||
int pq_drbg_is_initialized(void) {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
/* pq_drbg_randombytes is called by randombytes() below. */
|
||||
int pq_drbg_randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || !g_initialized) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
size_t avail;
|
||||
size_t to_copy;
|
||||
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
drbg_refill();
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
return -1; /* refill failed */
|
||||
}
|
||||
}
|
||||
|
||||
avail = sizeof(g_buffer) - g_buffer_pos;
|
||||
to_copy = (len < avail) ? len : avail;
|
||||
memcpy(buf, g_buffer + g_buffer_pos, to_copy);
|
||||
g_buffer_pos += to_copy;
|
||||
buf += to_copy;
|
||||
len -= to_copy;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* randombytes_mbedtls.c — randombytes() implementation for ESP32 firmware.
|
||||
*
|
||||
* PQClean's algorithm code calls randombytes() for:
|
||||
* 1. Key generation (keygen) — must be deterministic from the mnemonic
|
||||
* seed so keys are recoverable. The DRBG is initialized via
|
||||
* pq_drbg_init() before keygen, so randombytes() draws from the
|
||||
* deterministic stream.
|
||||
* 2. Encapsulation (ML-KEM enc) — needs real cryptographic randomness.
|
||||
* When the DRBG is NOT initialized, randombytes() falls back to
|
||||
* esp_fill_random() which uses the ESP32 hardware RNG.
|
||||
*
|
||||
* This dual-mode behavior matches the host build (src/pq_drbg.c) where
|
||||
* the DRBG is initialized for keygen and randombytes() returns -1 if
|
||||
* called without initialization. On firmware we allow the fallback to
|
||||
* hardware RNG for encaps, which is the correct behavior.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "esp_random.h"
|
||||
|
||||
/* Defined in pq_drbg_firmware.c */
|
||||
extern int pq_drbg_randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
/* Check if the DRBG is initialized (declared in pq_drbg_firmware.c).
|
||||
* We use a helper to avoid exposing the static directly. */
|
||||
extern int pq_drbg_is_initialized(void);
|
||||
|
||||
int randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* If the deterministic DRBG is active (keygen mode), use it. */
|
||||
if (pq_drbg_is_initialized()) {
|
||||
return pq_drbg_randombytes(buf, len);
|
||||
}
|
||||
|
||||
/* Otherwise, use the ESP32 hardware RNG for real randomness (encaps). */
|
||||
esp_fill_random(buf, len);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# Add-Ons
|
||||
|
||||
Here is a list of additional hardware add-ons that can add functionality to your CYD
|
||||
|
||||
## SD Card Sniffer
|
||||
|
||||
If you want to use the pins of the SD card for a different purpose, the easiest way to do that is with an "SD card sniffer", which basically plugs into the SD card slot and breaks out the pins. It's particularly useful for SPI devices.
|
||||
|
||||
## Pin-out of the Sniffer board
|
||||
|
||||
| Sniffer Board Label | ESP32 Pin | SPI Use |
|
||||
| ------------------- | --------- | --------- |
|
||||
| DAT2 | - | - |
|
||||
| CD | IO5 | CS |
|
||||
| CMD | IO23 | DI / MOSI |
|
||||
| GND | GND | - |
|
||||
| VCC | 3.3V | - |
|
||||
| CLK | IO18 | SCLK |
|
||||
| DAT0 | IO19 | DO / MISO |
|
||||
| DAT1 | - | - |
|
||||
|
||||
### Links
|
||||
|
||||
- [Micro SD Card Sniffer - Aliexpress\*](https://s.click.aliexpress.com/e/_Ddwcy9h)
|
||||
|
||||
## Nintendo Wii Nunchuck
|
||||
|
||||
A Nunchuck controller from a Nintendo Wii is a great input device for CYD projects as they are inexpensive and, since they use i2c for communication, they only require 2 GPIO pins to connect them up.
|
||||
|
||||
For these two pins you get:
|
||||
|
||||
- An analog stick
|
||||
- 2 Buttons
|
||||
- An accelerometer
|
||||
|
||||
### Hardware Required
|
||||
|
||||
#### Nunchuck controllers
|
||||
|
||||
Official Nintendo ones are generally better (maybe try second-hand options), but third-party ones also work fine.
|
||||
|
||||
- [Amazon.co.uk Search\*](https://amzn.to/3nQrXcE)
|
||||
- [Amazon.com Search\*](https://amzn.to/3nRJTUd)
|
||||
- [Aliexpress (Third Party)\*](https://s.click.aliexpress.com/e/_AaQbXh)
|
||||
|
||||
#### Nunchuck Adaptors
|
||||
|
||||
There are many different options available for these, even the cheap ones from Aliexpress work perfectly.
|
||||
|
||||
- [Aliexpress](https://s.click.aliexpress.com/e/_AEEtc3)
|
||||
- [My Open source one from Oshpark](https://oshpark.com/shared_projects/RcIxSx2D)
|
||||
- [Adafaruit](https://www.adafruit.com/product/4836)
|
||||
|
||||
### Wiring
|
||||
|
||||
The easiest way to wire this up is to use the wire that came with the CYD and the **CN1** JST connector (the one closest to the Micro SD card slot)
|
||||
|
||||
Connect the wire to your breakout board as follows:
|
||||
|
||||
| CYD CN1 | Adapter | Note |
|
||||
| ------- | ----------- | ------------------ |
|
||||
| GND | - (AKA GND) | Black wire for me |
|
||||
| 3.3V | + (AKA 3V) | Red wire for me |
|
||||
| IO22 | d (AKA SDA) | Blue wire for me |
|
||||
| IO27 | c (AKA SCL) | Yellow wire for me |
|
||||
|
||||
Note: I have found pull-ups resistors are not required on SDA and SCL
|
||||
|
||||
### Example
|
||||
|
||||
Check out the [NunchuckTest](/Examples/InputTests/NunchuckTest) example for code how to use it.
|
||||
|
||||
## Speakers
|
||||
|
||||
A speaker can be attached to the display with a 1.25mm JST connector to the connector labeled "SPEAK" (or soldered)
|
||||
|
||||
Check out the [HelloRadio](/Examples/Basics/7-HelloRadio) example for the code on how to use it.
|
||||
|
||||
Most small 8 Ohm speakers should work. Maybe worth adding a 1.25mm JST connector to it to make it easy to add remove.
|
||||
|
||||
### Links
|
||||
|
||||
- [Speaker with 1.25mm JST connector (2pcs) - Aliexpress\*](https://s.click.aliexpress.com/e/_DBOJoh7) - Tested, works right out of the package.
|
||||
- [2pin 1.25mm JST connectors - Aliexpress\*](https://s.click.aliexpress.com/e/_DlbPkWH) - Not purchased by me, but should work
|
||||
|
||||
\* = Affiliate Link - It doesn't cost you any extra but I receive a small portion of the sale.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Media and Mentions
|
||||
|
||||
This page can document any times the CYD project was mentioned somewhere!
|
||||
|
||||
## Videos
|
||||
|
||||
- [Brian Lough (hey, thats me!) - Cheap and Easy to Use ESP32 Screen!](https://www.youtube.com/watch?v=0AVyvwv0agk)
|
||||
- [Talking Sasquach - Don't be Fooled!! This Cheap Yellow Display Can Do a LOT!!](https://youtu.be/PsqMCoCTgTg?feature=shared)
|
||||
- [Teaching Tech - Cheap and easy Klipper touch interface with CYD Klipper](https://youtu.be/R3o0MGYW1ZU?feature=shared)
|
||||
|
||||
## Articles
|
||||
|
||||
- [Hackaday.com - “Cheap Yellow Display” Builds Community Through Hardware](https://hackaday.com/2023/10/28/cheap-yellow-display-builds-community-through-hardware/)
|
||||
- [Hackster.io - Brian Lough Looks to Build a Community Around the Espressif ESP32-Powered "Cheap Yellow Display"](https://www.hackster.io/news/brian-lough-looks-to-build-a-community-around-the-espressif-esp32-powered-cheap-yellow-display-66d23972910d)
|
||||
@@ -0,0 +1,140 @@
|
||||
# Pins
|
||||
|
||||
This page talks about the pins on the CYD.
|
||||
|
||||
## Connector types
|
||||
|
||||
The connectors are often called "1.25mm JST" but the correct name is "Molex PicoBlade".
|
||||
Chinese clones are sometimes called "mx1.25".
|
||||
|
||||
|Connector|Type |Note |
|
||||
|--- |--- |---- |
|
||||
|[**P1**](#p1) |4P 1.25mm Molex PicoBlade|Serial |
|
||||
|[**P3**](#p3) |4P 1.25mm Molex PicoBlade|GPIO |
|
||||
|[**P4**](#p4) |2P 1.25mm Molex PicoBlade|Speaker |
|
||||
|[**CN1**](#cn1)|4P 1.25mm Molex PicoBlade|GPIO (I2C) |
|
||||
|
||||
## What pins are available on the CYD?
|
||||
|
||||
There are 3 easily accessible GPIO pins
|
||||
|
||||
|Pin|Location|Note|
|
||||
|---|---|----|
|
||||
|IO35|**P3** Molex PicoBlade connector|Input only pin, no internal pull-ups available|
|
||||
|IO22|**P3** and **CN1** Molex PicoBlade connector||
|
||||
|IO27|**CN1** Molex PicoBlade connector||
|
||||
|
||||
If you need more than that, you need to start taking them from something else. An SD Card sniffer like mentioned in the [Add-ons](/ADDONS.md) is probably the next easiest.
|
||||
|
||||
After that you're probably de-soldering something!
|
||||
|
||||
## Broken Out Pins
|
||||
|
||||
There are three 4P 1.25mm Molex PicoBlade connectors on the board.
|
||||
|
||||
### P3
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO35||Input only pin, no internal pull-ups available|
|
||||
|IO22||Also on the **CN1** connector|
|
||||
|IO21||Used for the TFT Backlight, so not really usable|
|
||||
|
||||
### CN1
|
||||
This is a great candidate for I2C devices
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO22||Also on **P3** connector|
|
||||
|IO27|||
|
||||
|3.3V|||
|
||||
|
||||
### P1
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|VIN|||
|
||||
|IO1(?)|TX|Maybe possible to use as a GPIO?|
|
||||
|IO3(?)|RX|Maybe possible to use as a GPIO?|
|
||||
|GND|||
|
||||
|
||||
|
||||
## Buttons
|
||||
|
||||
The CYD has two buttons, reset and boot.
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO0|BOOT|Can be used as an input in sketches|
|
||||
|
||||
## Speaker
|
||||
|
||||
The speaker connector is a 2P 1.25mm Molex PicoBlade connector that is connected to the amplifier, so not usable as GPIO at the speaker connector
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO26|Connected to amp|`i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN);`|
|
||||
|
||||
## RGB LED
|
||||
|
||||
If your project requires additional pins to what is available elsewhere, this might be a good candidate to sacrifice.
|
||||
|
||||
Note: LEDs are "active low", meaning HIGH == off, LOW == on
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO4|Red LED||
|
||||
|IO16|Green LED||
|
||||
|IO17|Blue LED||
|
||||
|
||||
## SD Card
|
||||
Uses the VSPI
|
||||
Pin names are predefined in SPI.h
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO5|SS||
|
||||
|IO18|SCK||
|
||||
|IO19|MISO||
|
||||
|IO23|MOSI||
|
||||
|
||||
## Touch Screen
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO25|XPT2046_CLK||
|
||||
|IO32|XPT2046_MOSI||
|
||||
|IO33|XPT2046_CS||
|
||||
|IO36|XPT2046_IRQ||
|
||||
|IO39|XPT2046_MISO||
|
||||
|
||||
## LDR (Light Sensor)
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO34|||
|
||||
|
||||
## Display
|
||||
Uses the HSPI
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO2|TFT_RS|AKA: TFT_DC|
|
||||
|IO12|TFT_SDO|AKA: TFT_MISO|
|
||||
|IO13|TFT_SDI|AKA: TFT_MOSI|
|
||||
|IO14|TFT_SCK||
|
||||
|IO15|TFT_CS||
|
||||
|IO21|TFT_BL|Also on P3 connector, for some reason|
|
||||
|
||||
## Test points
|
||||
|Pad|Use|Note|
|
||||
|---|---|----|
|
||||
|S1|GND|near USB-SERIAL|
|
||||
|S2|3.3v|for ESP32|
|
||||
|S3|5v|near USB-SERIAL|
|
||||
|S4|GND|for ESP32|
|
||||
|S5|3.3v|for TFT|
|
||||
|JP0 (pad nearest USB socket)|5v|TFT LDO|
|
||||
|JP0|3.3v|TFT LDO|
|
||||
|JP3 (pad nearest USB socket)|5v|ESP32 LDO|
|
||||
|JP3|3.3v|ESP32 LDO|
|
||||
@@ -0,0 +1,60 @@
|
||||
# Projects
|
||||
|
||||
Because the CYD is a common platform, it makes it really useful for sharing projects. This page will be a list of projects that are available on the CYD.
|
||||
|
||||
## Disclaimer!
|
||||
|
||||
Projects appearing on here is not necessarily a seal of approval from me, I will not be test each project that gets added, so please install these projects at your own risk!
|
||||
|
||||
## Projects
|
||||
|
||||
| Name | Description | Author | Additional Hardware? | Project Page | WebFlash |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| Spotify DIY Thing | A device for displaying your currently playing Spotify track | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/Spotify-Diy-Thing) | [WebFlash](https://witnessmenow.github.io/Spotify-Diy-Thing/) |
|
||||
| F1 Notifier | Displays and notifies you of the F1 session times(in your local timezone) | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/F1-Arduino-Notifications) | [WebFlash](https://witnessmenow.github.io/F1-Arduino-Notifications/) |
|
||||
| Tetris with Nunchuck | A version of Tetris using a Nintendo wii Nunchuck | [Brian Lough](https://github.com/witnessmenow) | A nunchuck and an adaptor for connecting it | [Code](/Examples/Projects/TetrisWithNunchuck) | |
|
||||
| Galagino | An emulator for some classic arcade games (Galaga, Donkey Kong, Pacman, Digdug and Frogger) | [Till Harbaum](https://github.com/harbaum) | A nunchuck and an adaptor for connecting it. Speaker if you want sound | [Github](https://github.com/harbaum/galagino) | |
|
||||
| ESP32-fluid-simulation | A small fluid simulation with touch input | [Kenny Peng](https://github.com/colonelwatch) | | [Github](https://github.com/colonelwatch/ESP32-fluid-simulation) | |
|
||||
| ESP32-TV | Play Video Files on the ESP32 | [atomic14](https://github.com/atomic14) | Speaker if you want sound and possibly an IR receiver | [Github](https://github.com/atomic14/esp32-tv) | |
|
||||
| xtouch | "The xtouch screen is a revolutionary addition to your BambuLab Printer" | [xperiments-in](https://github.com/xperiments-in) (\#) | | [Github](https://github.com/xperiments-in/xtouch) | [Webflash](https://github.com/xperiments-in/xtouch#online-web-installer) |
|
||||
| CYD-Klipper | An implementation of a wireless Klipper status display on an ESP32 + screen | [Sims](https://github.com/suchmememanyskill) | | [Github](https://github.com/suchmememanyskill/CYD-Klipper) | [Webflash](https://suchmememanyskill.github.io/CYD-Klipper/) |
|
||||
| DRO (for lathe / mill) | A DRO (digital readout) for your lathe or mill | [Alanesq](https://github.com/alanesq) | It uses cheap digital caliper, requires a very basic interface | [Github](https://github.com/alanesq/DRO) | |
|
||||
| ESP32Marauder-CYD | A suite of WiFi/Bluetooth offensive and defensive tools for the ESP32 | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | GPS if you want BT/Wifi wardriving options | [Github](https://github.com/Fr4nkFletcher/ESP32-Marauder-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/Adafruit_WebSerial_ESPTool/) |
|
||||
| NerdMiner_v2 | A project that lets you try to solve a bitcoin block with a small piece of hardware. | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | | [Github](https://github.com/Fr4nkFletcher/NerdMiner_v2-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/NerdMiner_v2-Cheap-Yellow-Display/flash.html) |
|
||||
| Tasmota | Tasmota (with UI) on the CYD | ? (\#) | | [Templates](https://templates.blakadder.com/sunton_ESP32-2432S028.html) | [Webflash](https://tasmota.github.io/install/) |
|
||||
| BAM | A game engine featuring smooth scrolling tile map, sprites in layers with pixel precision on-screen collision detection, intuitive definition of game objects and logic, decent performance, ~30 frames per second on the device | [calint](https://github.com/calint) | | [Github](https://github.com/calint/bam) | |
|
||||
|London Underground Arrivals| A highly configurable application that replicates the train arrivals boards found in [TFL](https://tfl.gov.uk/) stations. All variable data is encoded in a json file that may be updated at any time without the need to recompile the application e.g. the station to be displayed or the time to refresh data from TFL. The source code already supports 2 variants of CYD and, I hope, contains clear instructions how to handle any other variant.| [David Henry](https://github.com/mgaman) | | [Github](https://github.com/mgaman/TFL-tube-arrivals-board-ESP32-TFT-Arduino) |
|
||||
|GitHub-Stats| This Arduino project fetches and displays GitHub repository statistics such as star count, open issues, forks and notifactions on a CYD or via serial communication. Ideal for developers to monitor project metrics in real time.| [ATOMNFT](https://github.com/ATOMNFT) | | [Github](https://github.com/ATOMNFT/ESP32-CYD-Projects/tree/main/GitHub-Stats) | |
|
||||
| Midbar-Firebase-Edition | An advanced password vault that stores the encrypted data in the cloud while keeping the cryptographic keys on the edge! | [Northstrix](https://github.com/Northstrix) | PS/2 keyboard and an optional STM32F103C8T6 (if you want it to emulate the USB keyboard) | [SourceForge](https://sourceforge.net/projects/midbar-firebase-edition/) [Github](https://github.com/Northstrix/Midbar-Firebase-Edition)
|
||||
| Electronic-Shelf-Label-Management-System | A simple device for displaying relevant product information. It gets the encrypted images via UDP. | [Northstrix](https://github.com/Northstrix)| | [SourceForge](https://sourceforge.net/projects/esl-management-system/) [Github](https://github.com/Northstrix/Electronic-Shelf-Label-Management-System)
|
||||
| ESP32-Tetris-With-Nintendo-64-Controller | Tetris for ESP32 with Nintendo 64 controller support | [Northstrix](https://github.com/Northstrix) | Nintendo 64 Controller and Arduino Nano | [SourceForge](https://sourceforge.net/projects/esp32-tetris/) [Github](https://github.com/Northstrix/ESP32-Tetris-With-Nintendo-64-Controller)
|
||||
| Midbar ESP32 CYD | A version of Midbar data vault tweaked specifically for the ESP32 Cheap Yellow Display. | [Northstrix](https://github.com/Northstrix) | PS/2 Keyboard | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd/) [Github](https://github.com/Northstrix/Midbar-ESP32-CYD)
|
||||
| ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase | An ESP32 CYD-based Electronic Shelf Label that makes use of the Google Firebase and AES-256. | [Northstrix](https://github.com/Northstrix) | | [SourceForge](https://sourceforge.net/projects/esp32-cyd-esl-with-firebase/) [Github](https://github.com/Northstrix/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase) | [WebFlash](https://northstrix.github.io/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase/flash.html) </br>!!! Format Flash area designated for SPIFFS with [ESP32 Filesystem Uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin/releases/) after using the WebFlash
|
||||
| Addressable RGB LED Strip Controller (The Lantern Project) | DIY Addressable RGB LED Strip Controller that utilizes the ESP32, ESP8266, and the WS2812 LED Strip. | [Northstrix](https://github.com/Northstrix) | Nintendo Wii Nunchuk, WiiChuck Nunchuck Adapter (PCB Board), ESP8266, 580 Ohm resistor, WS2812 LED Strip | [SourceForge](https://sourceforge.net/projects/the-lantern-project/) [Github](https://github.com/Northstrix/Lantern)
|
||||
| Midbar ESP32 CYD Firebase Edition | A version of Midbar data vault adapted for the ESP32 CYD and WebFlash. It keeps the cryptographic keys in the ESP32 RAM and stores the ciphertexts (encrypted data) in the Google Firebase. | [Northstrix](https://github.com/Northstrix) (Adapted for CYD2USB by [Rovel](https://github.com/Rovel))| PS2 Keyboard, PS2 Port *optional | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd-firebase/) [Github (CYD)](https://github.com/Northstrix/Midbar-ESP32-CYD-Firebase-Edition) [Github (CYD2USB)](https://github.com/Northstrix/Midbar-ESP32-CYD2USB-Firebase-Edition) | [WebFlash (CYD)](https://northstrix.github.io/Midbar-ESP32-CYD-Firebase-Edition/flash) [WebFlash (CYD2USB)](https://northstrix.github.io/Midbar-ESP32-CYD2USB-Firebase-Edition/flash)
|
||||
| cydOS (WIP) | cydOS is a GUI app that is able to manage various aspects of the CYD, like SD browsing and file mangement, on board flashing of .bin files for rapid firmware switching, on board device settings(WIP) | [orlandobianco](https://github.com/orlandobianco) | | [Github]((https://github.com/orlandobianco/cydOS)) | |
|
||||
| ESP32 MFA Authenticator | Turn the CYD into a MFA Authenticator | [AllanOricil](https://github.com/AllanOricil) | | [Github](https://github.com/AllanOricil/esp32-mfa-authenticator) | [Webflash](https://allanoricil.github.io/esp32-mfa-authenticator/)
|
||||
| cydWeatherStation | cyd Weather station | [gustheseventh](https://github.com/gustheseventh) (#) | | [Github](https://github.com/gustheseventh/cyd-Weather-Station) | |
|
||||
| PhilRadio | CYD Wifi Radio project. Re-using an old radio as hardware. Exposing a webserver on local network to configure the radio stations. Persistent storage. | [mogrikid](https://github.com/mogrikid) | Required: A speaker. Recommended: Speaker, potentiometer, 10kohm resistor, female usb port, switch | [Github](https://github.com/mogrikid/PhilRadio)
|
||||
| cydWeeWX | Simple CYD Weather Display for the open source [WeeWX](https://www.weewx.com/) weather station server. | [hcomet](https://hcomet.github.io/) | | [Github](https://github.com/hcomet/cydWeeWX)| [Webflash](https://hcomet.github.io/cydWeeWX/cydWeeWXFlash.html) |
|
||||
| CYD Stream Deck | A customizable touch-based Bluetooth HID controller using CYD. | [gahingwoo](https://github.com/gahingwoo) | | [GitHub](https://github.com/gahingwoo/cyd-stream-deck) | [Webflash](https://gahingwoo.github.io/cyd-stream-deck/webflash/index.html) |
|
||||
| CYD DHT22 Weather Clock | A weather and time display using CYD. | [gahingwoo](https://github.com/gahingwoo) | DHT22 sensor | [GitHub](https://github.com/gahingwoo/cyd-dht22-weather-clock) | |
|
||||
| ESP CYD MCP | Model Context Protocol (MCP) server implementation for the ESP32 CYD | [OfryL](https://github.com/OfryL) | | [Github](https://github.com/OfryL/esp-cyd-mcp) | |
|
||||
| Aura | Smart weather forecast device (OpenMeteo) | [Surrey-Homeware](https://github.com/Surrey-Homeware/) (\#) | [3D printed case](https://makerworld.com/en/models/1382304-aura-smart-weather-forecast-display#profileId-1430951) | [Github](https://github.com/Surrey-Homeware/Aura) | [Webflash](https://surrey-homeware.github.io/aura-installer/) |
|
||||
| SmartEnergyMeter | An ESPHome display for Energy in the house (solar, battery, etc) | [anthony-spruyt](https://github.com/anthony-spruyt) (#) | | [Github](https://github.com/anthony-spruyt/ESPHOME-ESP32_CYD_V2-SmartEnergyMeter) | |
|
||||
| Navi Phone | Relica of the Mobile Phones used int the anime Serial Experments Lain | [Aquafrostbyte](https://github.com/AquaFrostByte) (#) | A Sd card is required, Speaker and Wifi is optional | [Github](https://github.com/AquaFrostByte/Navi-Phone) | |
|
||||
| OASMan | Open-source Air Suspension Management - Worlds first DIY Digital Air suspension controller for your car! | [gopro_2027](https://github.com/gopro2027/) | Ideally you would build the manifold and install it in your car, but if you just want to test the connection you can use an original esp32 dev board and flash the manifold code through platformio. We also have a [3d printable case](https://github.com/gopro2027/ArduinoAirSuspensionController/blob/main/3d%20Prints/other/3.2%20inch%20screen%20case/3.2%20inch%20CYD%20screen%20container%20v19%20-%20gopro_2027's%20design.stl) | [Github](https://github.com/gopro2027/ArduinoAirSuspensionController) | [Webflash](https://oasman.dev/oasman/flash/) |
|
||||
| Sonos Remote Control | Use your Sonos Speakers as Internet Radio with Station Buttons | Florian Lenz | https://github.com/SpringTideSystems | [GitHub](https://github.com/SpringTideSystems/CYD_Sonos-RemoteControl) | |
|
||||
|
||||
(\#) = Project not added by original author
|
||||
|
||||
## Adding a project
|
||||
|
||||
If you have a project that you would like to add, please feel free to add it to the list!
|
||||
|
||||
New projects should be added to bottom of the list.
|
||||
|
||||
Some rules:
|
||||
|
||||
- Project must be open source
|
||||
- Project must be functional - It's ok for it to not be finished, but it should do what it says!
|
||||
@@ -0,0 +1,114 @@
|
||||
# ESP32-Cheap-Yellow-Display
|
||||
|
||||
There is an ESP32 with a built in 320 x 240 2.8" LCD display with a touch screen called the "ESP32-2432S028R", since this doesn't roll of the tongue, I propose it should be renamed the "Cheap Yellow Display" or CYD for short. This display is only about $15 delivered so I think it's really good value.
|
||||
|
||||

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

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

|
||||
|
||||
## My CYD has two USB ports
|
||||
|
||||
The original CYD only has a micro USB port, but there is a device that is also labelled a _ESP32-2432S028_ that has two USB ports, one micro USB and one USB-C.
|
||||
|
||||
Having an additional USB port would be a minor problem if that was the only difference, but unfortunately the display also works differently, the colours are inverted on the display.
|
||||
|
||||
It can be fixed in a couple of ways:
|
||||
|
||||
- Use platformio - The examples on the Github have all been updated so they can be used with platformio, and you can simply select CYD or CYD2USB and it will just work
|
||||
- Use the [CYD2USB specific User_setup.h](/DisplayConfig/CYD2USB/) that is on the repo, you can now use all the examples like normal
|
||||
- Invert the display at the code level using the `tft.invertDisplay(1);` method
|
||||
|
||||
### The USB-C port doesn't work
|
||||
|
||||
The USB-C port has a flaw in it, it doesn’t have the resistors on the CC lines. This means it will not work with USB-C to USB-C cables. If your computer only has USB-C ports, you can use it through a USB-C to USB-A adaptor.
|
||||
|
||||
### The Display doesn't look as good
|
||||
|
||||
There seems to be a gamma issue with the CYD2USB (I don't even know what gamma is)
|
||||
|
||||
Adding this to the code seems to help
|
||||
|
||||
```
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(2);
|
||||
delay(120);
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(1);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/ecp.h"
|
||||
#include "mbedtls/pk.h"
|
||||
#include "psa/crypto.h"
|
||||
|
||||
#include "secp256k1.h"
|
||||
#include "secp256k1_extrakeys.h"
|
||||
#include "secp256k1_schnorrsig.h"
|
||||
|
||||
static const char *KD_TAG = "key_derivation";
|
||||
|
||||
#define BIP32_HARDENED_FLAG 0x80000000u
|
||||
|
||||
typedef struct {
|
||||
@@ -253,3 +261,351 @@ 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. */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
|
||||
/* Derive the ed25519 public key via PSA crypto (IDF v5.x mbedtls has no
|
||||
* mbedtls_ed25519_make_public). Import the private key, export the pub. */
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_import failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_export_public failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ed25519 sign via PSA (PureEdDSA — signs the raw message, not pre-hashed). */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]) {
|
||||
return ed25519_sign_msg(privkey, msg32, 32, sig64);
|
||||
}
|
||||
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t sig_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 sign psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_sign_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig64, 64, &sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || sig_len != 64) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_sign_message failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, pubkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 verify psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_verify_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig, sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
return (status == PSA_SUCCESS) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --- x25519 --- */
|
||||
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]) {
|
||||
/* m/44'/102002'/<index>'/0'/0' — all hardened (SLIP-0010) */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102002u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t derived_seed[32];
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* The SLIP-0010 derived 32 bytes IS the x25519 private key.
|
||||
* PSA imports it and exports the public key (PSA handles clamping). */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_DERIVE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_ECDH);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_import failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_export_public failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ML-DSA-65 --- */
|
||||
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102003'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102003u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = fw_pq_ml_dsa_65_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- SLH-DSA-128s --- */
|
||||
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102004'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102004u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ESP_LOGW(KD_TAG, "SLH-DSA-128s keygen: this takes 5-30 seconds on ESP32");
|
||||
int ret = fw_pq_slh_dsa_128s_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- ML-KEM-768 --- */
|
||||
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk) {
|
||||
/* m/44'/102005'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
102005u | BIP32_HARDENED_FLAG,
|
||||
index | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t pq_seed[32];
|
||||
|
||||
if (seed == NULL || pk == NULL || sk == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = fw_pq_ml_kem_768_keygen(pq_seed, pk, sk);
|
||||
memset(pq_seed, 0, sizeof(pq_seed));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- secp256k1 (Nostr, existing) --- */
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
|
||||
|
||||
/* --- ed25519 (SSH signatures) --- */
|
||||
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
|
||||
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
|
||||
* privkey: 32-byte ed25519 private scalar
|
||||
* pubkey: 32-byte ed25519 public key
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
|
||||
/* Signs a 32-byte message digest with ed25519 (PureEdDSA, raw message).
|
||||
* sig: 64-byte ed25519 signature
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Signs a variable-length message with ed25519 (PureEdDSA). */
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Verifies an ed25519 signature over a variable-length message. */
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]);
|
||||
|
||||
/* --- x25519 (age encryption / key agreement) --- */
|
||||
/* Derives an x25519 keypair from the mnemonic seed using SLIP-0010
|
||||
* all-hardened derivation: m/44'/102002'/<n>'/0'/0'
|
||||
* privkey: 32-byte x25519 private scalar
|
||||
* pubkey: 32-byte x25519 public key
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
|
||||
/* --- ML-DSA-65 (post-quantum signatures, FIPS 204) --- */
|
||||
/* Derives an ML-DSA-65 keypair from the mnemonic seed.
|
||||
* Path: m/44'/102003'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_ML_DSA_65_PUBKEY_LEN (1952) bytes — caller must allocate
|
||||
* sk: FW_ML_DSA_65_PRIVKEY_LEN (4032) bytes — caller must allocate
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* --- SLH-DSA-128s (post-quantum hash-based signatures, FIPS 205) --- */
|
||||
/* Derives an SLH-DSA-128s keypair from the mnemonic seed.
|
||||
* Path: m/44'/102004'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_SLH_DSA_128S_PUBKEY_LEN (32) bytes
|
||||
* sk: FW_SLH_DSA_128S_PRIVKEY_LEN (64) bytes
|
||||
* WARNING: Takes 5-30 seconds on ESP32.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
/* --- ML-KEM-768 (post-quantum KEM, FIPS 203) --- */
|
||||
/* Derives an ML-KEM-768 keypair from the mnemonic seed.
|
||||
* Path: m/44'/102005'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
|
||||
* pk: FW_ML_KEM_768_PUBKEY_LEN (1184) bytes — caller must allocate
|
||||
* sk: FW_ML_KEM_768_PRIVKEY_LEN (2400) bytes — caller must allocate
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t *pk, uint8_t *sk);
|
||||
|
||||
+1706
-402
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -8,3 +8,8 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
|
||||
# PSA crypto for ed25519 sign/verify (IDF v5.x mbedtls has no mbedtls_ed25519_*).
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
# Curve25519 / Ed25519 ECP domain parameter (already enabled, kept for clarity).
|
||||
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# CMakeLists.txt — ESP-IDF component for PQClean post-quantum algorithms.
|
||||
#
|
||||
# Compiles the three PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
|
||||
# from the shared resources/pqclean/ source tree, using the mbedtls
|
||||
# crypto backend (crypto_backend_mbedtls.c) for SHA-2/SHA3/SHAKE.
|
||||
#
|
||||
# The source files are referenced via relative paths back to the shared
|
||||
# resources/pqclean/ directory so there is a single source of truth.
|
||||
#
|
||||
# mbedtls requirements:
|
||||
# CONFIG_MBEDTLS_SHA3_C=y (for SHA3-256, SHA3-512)
|
||||
# CONFIG_MBEDTLS_SHAKE_C=y (for SHAKE-128, SHAKE-256)
|
||||
# Enable these in menuconfig under Component config -> mbedTLS ->
|
||||
# Hash functions -> SHA-3 and SHAKE.
|
||||
|
||||
set(PQCLEAN_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../resources/pqclean")
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/sign.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/fors.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/wots.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/hash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/thash.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/address.c"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/utils.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/kem.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/indcpa.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/poly.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/ntt.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/cbd.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/reduce.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/symmetric.c"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/verify.c"
|
||||
"${PQCLEAN_ROOT}/common/fips202.c"
|
||||
"${PQCLEAN_ROOT}/common/sha2.c"
|
||||
"${PQCLEAN_ROOT}/common/crypto_backend_mbedtls.c"
|
||||
"randombytes_mbedtls.c"
|
||||
"pq_drbg_firmware.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
"${PQCLEAN_ROOT}/common"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65"
|
||||
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s"
|
||||
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768"
|
||||
REQUIRES
|
||||
mbedtls
|
||||
)
|
||||
|
||||
# Suppress warnings from the PQClean code (it uses C99 patterns that
|
||||
# trigger -Wextra warnings under ESP-IDF's default flags).
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
-Wno-unused-parameter
|
||||
-Wno-sign-compare
|
||||
-Wno-unused-variable
|
||||
-Wno-unused-but-set-variable
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_dsa_65_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_DSA_65_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/ml-dsa-65/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
/* ml_kem_768_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#define FIRMWARE_ML_KEM_768_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_kem/ml-kem-768/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
/* pqclean.h — Umbrella include for the ESP32 firmware PQClean component.
|
||||
*
|
||||
* Exposes the three post-quantum algorithms (ML-DSA-65, SLH-DSA-128s,
|
||||
* ML-KEM-768) and the deterministic DRBG used for mnemonic-recoverable
|
||||
* key generation.
|
||||
*
|
||||
* On ESP32 the underlying hash/SHAKE primitives are provided by the
|
||||
* mbedtls backend (crypto_backend_mbedtls.c) instead of OpenSSL.
|
||||
*/
|
||||
#ifndef FIRMWARE_PQCLEAN_H
|
||||
#define FIRMWARE_PQCLEAN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* --- ML-DSA-65 (FIPS 204, lattice signatures) --- */
|
||||
#include "ml_dsa_65_api.h"
|
||||
|
||||
/* --- SLH-DSA-128s (FIPS 205, hash-based signatures) --- */
|
||||
#include "slh_dsa_128s_api.h"
|
||||
|
||||
/* --- ML-KEM-768 (FIPS 203, lattice KEM) --- */
|
||||
#include "ml_kem_768_api.h"
|
||||
|
||||
/* --- Deterministic DRBG (replaces randombytes() for keygen) --- */
|
||||
/* Initializes the DRBG with a 32-byte mnemonic-derived seed. Subsequent
|
||||
* randombytes() calls will produce a deterministic byte stream. */
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
|
||||
|
||||
/* Zeroizes the DRBG state (call after keygen to wipe sensitive material). */
|
||||
void pq_drbg_zeroize(void);
|
||||
|
||||
/* randombytes() — called by the PQClean algorithm code.
|
||||
* On firmware this is provided by randombytes_mbedtls.c (deterministic DRBG
|
||||
* for keygen, or mbedtls_ctr_drbg for real randomness during encaps). */
|
||||
int randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
#endif /* FIRMWARE_PQCLEAN_H */
|
||||
@@ -0,0 +1,5 @@
|
||||
/* slh_dsa_128s_api.h — firmware wrapper that includes the real PQClean header. */
|
||||
#ifndef FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#define FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
|
||||
#include "../../../../resources/pqclean/crypto_sign/slh-dsa-128s/api.h"
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
/* pq_drbg_firmware.c — Deterministic PRNG for PQ key generation on ESP32.
|
||||
*
|
||||
* Same algorithm as the host's src/pq_drbg.c but uses the crypto backend
|
||||
* abstraction (which resolves to mbedtls on ESP32) for SHAKE-256 instead
|
||||
* of OpenSSL EVP. This allows deterministic PQ key generation from a
|
||||
* mnemonic-derived seed: same seed -> same randombytes output sequence.
|
||||
*
|
||||
* The PRNG: SHAKE-256(seed || counter) produces a stream of pseudo-random
|
||||
* bytes. The counter is a 64-bit little-endian integer that increments
|
||||
* each time we need more output.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "crypto_backend.h"
|
||||
|
||||
/* --- DRBG state --- */
|
||||
|
||||
static unsigned char g_seed[32];
|
||||
static int g_seed_len = 0;
|
||||
static uint64_t g_counter = 0;
|
||||
static unsigned char g_buffer[168]; /* SHAKE-256 rate = 136, 168 for safety */
|
||||
static size_t g_buffer_pos = sizeof(g_buffer);
|
||||
static int g_initialized = 0;
|
||||
|
||||
/* --- internal: squeeze more bytes from SHAKE-256 --- */
|
||||
|
||||
static void drbg_refill(void) {
|
||||
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
|
||||
|
||||
memcpy(seed_block, g_seed, (size_t)g_seed_len);
|
||||
seed_block[g_seed_len + 0] = (unsigned char)(g_counter & 0xFF);
|
||||
seed_block[g_seed_len + 1] = (unsigned char)((g_counter >> 8) & 0xFF);
|
||||
seed_block[g_seed_len + 2] = (unsigned char)((g_counter >> 16) & 0xFF);
|
||||
seed_block[g_seed_len + 3] = (unsigned char)((g_counter >> 24) & 0xFF);
|
||||
seed_block[g_seed_len + 4] = (unsigned char)((g_counter >> 32) & 0xFF);
|
||||
seed_block[g_seed_len + 5] = (unsigned char)((g_counter >> 40) & 0xFF);
|
||||
seed_block[g_seed_len + 6] = (unsigned char)((g_counter >> 48) & 0xFF);
|
||||
seed_block[g_seed_len + 7] = (unsigned char)((g_counter >> 56) & 0xFF);
|
||||
|
||||
crypto_backend_shake256(seed_block, (size_t)g_seed_len + 8,
|
||||
g_buffer, sizeof(g_buffer));
|
||||
|
||||
g_counter++;
|
||||
g_buffer_pos = 0;
|
||||
}
|
||||
|
||||
/* --- public API --- */
|
||||
|
||||
void pq_drbg_init(const unsigned char *seed, size_t seed_len) {
|
||||
if (seed == NULL || seed_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_seed, 0, sizeof(g_seed));
|
||||
if (seed_len > sizeof(g_seed)) {
|
||||
seed_len = sizeof(g_seed);
|
||||
}
|
||||
memcpy(g_seed, seed, seed_len);
|
||||
g_seed_len = (int)sizeof(g_seed); /* always use 32-byte seed (zero-padded) */
|
||||
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 1;
|
||||
}
|
||||
|
||||
void pq_drbg_zeroize(void) {
|
||||
crypto_backend_cleanse(g_seed, sizeof(g_seed));
|
||||
crypto_backend_cleanse(g_buffer, sizeof(g_buffer));
|
||||
g_seed_len = 0;
|
||||
g_counter = 0;
|
||||
g_buffer_pos = sizeof(g_buffer);
|
||||
g_initialized = 0;
|
||||
}
|
||||
|
||||
/* Returns 1 if the DRBG has been initialized (keygen mode), 0 otherwise.
|
||||
* Used by randombytes_mbedtls.c to decide between deterministic DRBG and
|
||||
* hardware RNG. */
|
||||
int pq_drbg_is_initialized(void) {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
/* pq_drbg_randombytes is called by randombytes() below. */
|
||||
int pq_drbg_randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || !g_initialized) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
size_t avail;
|
||||
size_t to_copy;
|
||||
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
drbg_refill();
|
||||
if (g_buffer_pos >= sizeof(g_buffer)) {
|
||||
return -1; /* refill failed */
|
||||
}
|
||||
}
|
||||
|
||||
avail = sizeof(g_buffer) - g_buffer_pos;
|
||||
to_copy = (len < avail) ? len : avail;
|
||||
memcpy(buf, g_buffer + g_buffer_pos, to_copy);
|
||||
g_buffer_pos += to_copy;
|
||||
buf += to_copy;
|
||||
len -= to_copy;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* randombytes_mbedtls.c — randombytes() implementation for ESP32 firmware.
|
||||
*
|
||||
* PQClean's algorithm code calls randombytes() for:
|
||||
* 1. Key generation (keygen) — must be deterministic from the mnemonic
|
||||
* seed so keys are recoverable. The DRBG is initialized via
|
||||
* pq_drbg_init() before keygen, so randombytes() draws from the
|
||||
* deterministic stream.
|
||||
* 2. Encapsulation (ML-KEM enc) — needs real cryptographic randomness.
|
||||
* When the DRBG is NOT initialized, randombytes() falls back to
|
||||
* esp_fill_random() which uses the ESP32 hardware RNG.
|
||||
*
|
||||
* This dual-mode behavior matches the host build (src/pq_drbg.c) where
|
||||
* the DRBG is initialized for keygen and randombytes() returns -1 if
|
||||
* called without initialization. On firmware we allow the fallback to
|
||||
* hardware RNG for encaps, which is the correct behavior.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "esp_random.h"
|
||||
|
||||
/* Defined in pq_drbg_firmware.c */
|
||||
extern int pq_drbg_randombytes(unsigned char *buf, size_t len);
|
||||
|
||||
/* Check if the DRBG is initialized (declared in pq_drbg_firmware.c).
|
||||
* We use a helper to avoid exposing the static directly. */
|
||||
extern int pq_drbg_is_initialized(void);
|
||||
|
||||
int randombytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* If the deterministic DRBG is active (keygen mode), use it. */
|
||||
if (pq_drbg_is_initialized()) {
|
||||
return pq_drbg_randombytes(buf, len);
|
||||
}
|
||||
|
||||
/* Otherwise, use the ESP32 hardware RNG for real randomness (encaps). */
|
||||
esp_fill_random(buf, len);
|
||||
return 0;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,192 @@
|
||||
# n_signer FPGA Signing Core
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
An FPGA-based secp256k1 signing core that provides the **maximum physical
|
||||
security** possible for Nostr signing: constant-time crypto (no instruction
|
||||
timing leakage), key material in FPGA fabric (no bus access to the key), and no
|
||||
firmware (no malware injection surface). The FPGA is a **signing oracle** — a
|
||||
small, auditable hardware module that does one thing (secp256k1 schnorr/ECDSA
|
||||
signing) with side-channel resistance that software on an MCU cannot match.
|
||||
|
||||
## The secure element gap
|
||||
|
||||
Commercial secure elements (NXP JCOP, Infineon, Microchip ATECC) support NIST
|
||||
curves (P-256, P-384) and RSA, but **not secp256k1** — the smart card industry
|
||||
standardized on NIST curves, and secp256k1 was treated as a "Bitcoin curve"
|
||||
that didn't get hardware support. This is why every Nostr/Bitcoin hardware
|
||||
wallet (Coldcard, Ledger, Trezor, Keystone) uses a **general-purpose MCU**
|
||||
running software secp256k1, not a secure element.
|
||||
|
||||
An FPGA fills this gap: it gives us **hardware-level secp256k1** without relying
|
||||
on a secure element vendor to support the curve. We write the secp256k1 core
|
||||
ourselves in Verilog, with full control over the timing, the key storage, and
|
||||
the side-channel resistance.
|
||||
|
||||
## Why an FPGA
|
||||
|
||||
| Property | MCU (software) | FPGA (hardware) |
|
||||
|---|---|---|
|
||||
| Timing leakage | Branch prediction, cache, instruction timing | **None** — fixed datapath, every op takes the same cycles |
|
||||
| Key storage | RAM (accessible via bus/debug) | **FPGA fabric / BRAM** (no external bus access) |
|
||||
| Firmware attacks | OS, USB stack, BT stack = injection surface | **No firmware** — bitstream is the entire program |
|
||||
| Debug access | JTAG/SWD can read RAM | **No debug path to key** if not routed |
|
||||
| Auditability | Large codebase (thousands of lines of C) | **Small Verilog core** (~2000 lines, auditable) |
|
||||
| PQ crypto | Yes (software) | No (too complex for FPGA) |
|
||||
|
||||
## Architecture: hybrid FPGA + MCU
|
||||
|
||||
The practical design is a **two-chip hybrid**: the FPGA is the signing oracle,
|
||||
the MCU handles the protocol/UI/transport. The MCU sends a message hash + key
|
||||
index to the FPGA over SPI; the FPGA signs with the key in fabric; the FPGA
|
||||
returns the 64-byte signature. The private key never leaves the FPGA.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Host_Side
|
||||
Host[Host: laptop/phone<br/>n_signer client]
|
||||
end
|
||||
subgraph Signer_Device
|
||||
MCU[MCU: RP2040 or nRF52840<br/>protocol + UI + transport<br/>PQ crypto in software]
|
||||
FPGA[FPGA: iCE40-UP5K<br/>secp256k1 signing core<br/>ed25519 signing core<br/>SHA-256/512 cores<br/>key in BRAM]
|
||||
Display[OLED / e-paper display]
|
||||
Buttons[approve / deny buttons]
|
||||
end
|
||||
Host -->|USB / IR / NFC / BLE| MCU
|
||||
MCU -->|SPI: msg_hash + key_index| FPGA
|
||||
FPGA -->|SPI: 64-byte signature| MCU
|
||||
MCU --> Display
|
||||
Buttons --> MCU
|
||||
MCU -->|response| Host
|
||||
```
|
||||
|
||||
### Division of labor
|
||||
|
||||
| Function | Chip | Notes |
|
||||
|---|---|---|
|
||||
| Transport (USB/IR/NFC/BLE) | MCU | Ported from CYD/Teensy firmware |
|
||||
| JSON-RPC dispatch | MCU | Ported from `handle_request()` |
|
||||
| Auth envelope verify | MCU | secp256k1 schnorr verify (software) |
|
||||
| Approval UI (display + buttons) | MCU | Ported from CYD UI |
|
||||
| Mnemonic entry | MCU | BIP-39 wordlist + entry UI |
|
||||
| BIP-32 / SLIP-0010 key derivation | **FPGA** | SHA-512 HMAC core + derivation FSM |
|
||||
| secp256k1 schnorr sign | **FPGA** | Constant-time scalar multiply + schnorr |
|
||||
| secp256k1 ECDSA sign | **FPGA** | Same scalar multiply + RFC 6979 nonce |
|
||||
| ed25519 sign | **FPGA** | Curve25519 arithmetic core |
|
||||
| x25519 ECDH | **FPGA** | Same curve as ed25519 |
|
||||
| SHA-256 | **FPGA** | Hardware core (~1000 LUTs) |
|
||||
| SHA-512 | **FPGA** | Hardware core (~2000 LUTs) — needed for BIP-32 |
|
||||
| HMAC-SHA-256 | **FPGA** | SHA-256 core + FSM — for the `derive` verb |
|
||||
| NIP-04 / NIP-44 encryption | MCU | AES + ChaCha20 in software |
|
||||
| ML-DSA-65 / SLH-DSA-128s / ML-KEM-768 | MCU | PQClean in software (too complex for FPGA) |
|
||||
| nostr_mine_event (PoW) | MCU | SHA-256 hash loop in software (or offload to FPGA) |
|
||||
|
||||
### Key storage in the FPGA
|
||||
|
||||
The mnemonic seed (64 bytes) is loaded into the FPGA's BRAM at boot (sent by
|
||||
the MCU after the user enters the mnemonic). The FPGA derives secp256k1/ed25519/
|
||||
x25519 keys on demand using its SHA-512 + modular arithmetic cores. The derived
|
||||
private keys live in FPGA registers/BRAM and are **never readable from the SPI
|
||||
interface** — the SPI interface only accepts "sign this hash with key index N"
|
||||
commands and returns signatures. There is no "read key" command.
|
||||
|
||||
This is the key security property: **the private key is physically unreachable
|
||||
from any external interface.** On an MCU, the key is in RAM and can be read via
|
||||
JTAG/SWD or a firmware exploit. On the FPGA, the key is in fabric and there is
|
||||
no path to it.
|
||||
|
||||
## FPGA board options
|
||||
|
||||
| Board | FPGA | LUTs | Toolchain | Price | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| **iCE40-UP5K** (e.g. iCEBreaker, Fomu) | iCE40UP5K | 5,300 | **Yosys + nextpnr** (open-source) | ~$15-20 | Best DIY choice. Open-source toolchain, DSP blocks, 128 KB BRAM, SPI flash. |
|
||||
| **Gowin Tang Nano 9K** | GW1NR-9 | 8,640 | Yosys + nextpnr (open-source) | ~$8 | Cheapest. Newer open-source support. |
|
||||
| **Lattice ECP5** (OrangeCrab, ULX3S) | LFE5U-12F / 25F / 45F | 12K-45K | Yosys + nextpnr (open-source) | ~$30-50 | More room. Supports **bitstream encryption** (important for production). |
|
||||
| **Xilinx Artix-7** (Arty A7) | XC7A35T | 33,280 | Vivado (proprietary) | ~$100 | Professional. Overkill for a signer. |
|
||||
|
||||
**Recommendation: Lattice iCE40-UP5K for prototyping, ECP5 for production.**
|
||||
The iCE40 has the most mature open-source toolchain (Yosys + nextpnr) and is
|
||||
cheap. The ECP5 adds bitstream encryption (prevents bitstream cloning) for a
|
||||
production device.
|
||||
|
||||
## The secp256k1 core (the hard part)
|
||||
|
||||
The secp256k1 signing core is the main development effort. It needs:
|
||||
|
||||
1. **256-bit modular arithmetic** over the secp256k1 prime field (p = 2²⁵⁶ - 2³² - 977):
|
||||
- Modular add, subtract, multiply (Montgomery multiplication for performance)
|
||||
- Modular inversion (Fermat's little theorem: a^(p-2) mod p, or extended Euclidean)
|
||||
2. **Point operations** on the secp256k1 curve (y² = x³ + 7):
|
||||
- Point addition (Jacobian coordinates)
|
||||
- Point doubling
|
||||
- Scalar multiplication (constant-time double-and-add, no conditional branches)
|
||||
3. **Schnorr sign** (BIP-340):
|
||||
- Deterministic nonce: k = HMAC-SHA256(d, x) where d is the key, x is the message hash
|
||||
- R = k·G (point multiplication)
|
||||
- e = tagged hash(R.x || P || m) (SHA-256)
|
||||
- s = (k + e·x) mod n (scalar multiply + modular add)
|
||||
- Signature = (R.x, s)
|
||||
4. **ECDSA sign** (for the `scheme:"ecdsa"` option):
|
||||
- RFC 6979 deterministic nonce: k = HMAC-SHA256(x, m) with rejection sampling
|
||||
- R = k·G
|
||||
- r = R.x mod n
|
||||
- s = k⁻¹ · (m + r·x) mod n
|
||||
- Signature = (r, s)
|
||||
|
||||
**Estimated size:** ~2000-5000 LUTs for the modular arithmetic + point
|
||||
multiplication + schnorr/ECDSA FSM. Fits comfortably in an iCE40-UP5K (5,300
|
||||
LUTs) alongside the SHA-256/512 cores and the SPI interface.
|
||||
|
||||
**Estimated sign time:** ~1-5 ms at 12 MHz (iCE40-UP5K typical clock). The
|
||||
bottleneck is the 256-bit modular multiplication (~0.5-2 ms per multiply, ~256
|
||||
multiplies per scalar multiplication). This is **much faster than software**
|
||||
(the CYD's software schnorr sign takes ~10-50 ms).
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Pure FPGA vs hybrid?** A pure-FPGA signer (no MCU) would implement the
|
||||
entire dispatch + transport + UI in Verilog. This is extremely secure but
|
||||
very hard to build (JSON parsing in Verilog is painful). The hybrid (FPGA
|
||||
signing oracle + MCU protocol) is practical and still gives the key-isolation
|
||||
benefit. **Lean toward hybrid.**
|
||||
- **Which MCU?** RP2040 (cheap, no WiFi) or nRF52840 (low power, NFC, BLE)?
|
||||
This determines the transport options (USB, IR, NFC, BLE).
|
||||
- **Bitstream security:** iCE40 doesn't support bitstream encryption. If an
|
||||
attacker reads the SPI flash, they get the bitstream (but not the key — the
|
||||
key is loaded at runtime by the MCU, not stored in the bitstream). For
|
||||
production, use ECP5 with bitstream encryption.
|
||||
- **Key loading:** the MCU sends the mnemonic seed to the FPGA at boot over
|
||||
SPI. Is this SPI transfer vulnerable to sniffing? It happens once, at boot,
|
||||
inside the device. If the device is physically sealed, the SPI bus is not
|
||||
accessible. For higher security, the FPGA could derive the key from the
|
||||
mnemonic internally (the MCU sends the mnemonic string, the FPGA does
|
||||
PBKDF2-HMAC-SHA512 + BIP-32 derivation in hardware).
|
||||
- **ed25519 core:** worth implementing, or secp256k1-only? ed25519 is the same
|
||||
field size (256-bit) but a different curve (Curve25519 vs secp256k1). The
|
||||
modular arithmetic is similar but the curve operations differ. Adding
|
||||
ed25519 roughly doubles the core size.
|
||||
- **Open-source secp256k1 FPGA cores:** are there existing Verilog secp256k1
|
||||
cores we can reuse or adapt? (There are Bitcoin mining cores, but those do
|
||||
double-SHA256, not ECDSA. Academic ECDSA-on-FPGA papers exist but the code
|
||||
is rarely open-sourced. We'd likely write the core from scratch.)
|
||||
|
||||
## Comparison to the other concepts
|
||||
|
||||
| | FPGA signer (hybrid) | MCU-only (CYD/Teensy) | Secure element |
|
||||
|---|---|---|---|
|
||||
| secp256k1 side-channel resistance | **Best** (constant-time, key in fabric) | Medium (software, timing leakage) | N/A (no secp256k1 support) |
|
||||
| Key isolation | **Best** (no bus access to key) | Low (RAM, JTAG/SWD accessible) | Best (tamper-resistant) |
|
||||
| Firmware attack surface | **Minimal** (no firmware on FPGA) | Large (OS, USB, BT stacks) | Minimal (fixed function) |
|
||||
| PQ crypto | On MCU (software) | On MCU (software) | N/A |
|
||||
| Development effort | **High** (Verilog secp256k1 core) | Low (port existing C code) | High (NDA + Java Card) |
|
||||
| Cost | ~$15 FPGA + ~$4 MCU = ~$20 | ~$4-27 (MCU only) | ~$3-5 (chip only) |
|
||||
| Auditability | **High** (small Verilog core, open toolchain) | Medium (large C codebase) | Low (proprietary, NDA) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide: hybrid (FPGA + MCU) vs pure FPGA
|
||||
- Decide: iCE40-UP5K (prototype) vs ECP5 (production)
|
||||
- Decide: secp256k1-only vs secp256k1 + ed25519
|
||||
- Survey existing open-source secp256k1 / ECDSA FPGA cores
|
||||
- Write a Verilog secp256k1 modular arithmetic core (the foundation)
|
||||
- Write a plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
@@ -0,0 +1,149 @@
|
||||
# n_signer IR Air-Gap Signer
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
A hardware signer that communicates with the host via **infrared light** —
|
||||
line-of-sight, short-range, physically directional. The signer never touches
|
||||
the host electrically: no wire, no radio, no shared ground. The only channel
|
||||
is modulated light through air. A small **USB receiver dongle** on the host
|
||||
decodes the IR signal and presents it as a CDC-ACM serial port.
|
||||
|
||||
This is the strongest air-gap model in the n_signer family: the signer is
|
||||
electrically isolated from the host, and the receiver dongle is a dumb
|
||||
IR-to-serial bridge with no crypto, no keys, and ~200 lines of auditable
|
||||
firmware.
|
||||
|
||||
## Concept
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: laptop<br/>n_signer client] -->|USB CDC| Dongle[USB IR receiver dongle<br/>RP2040 + IR receiver]
|
||||
Dongle -->|IR light<br/>line-of-sight| Signer[IR signer<br/>RP2040 + OLED + buttons]
|
||||
Signer -->|approve/deny button| User[User]
|
||||
Signer -->|IR light response| Dongle
|
||||
Dongle -->|USB CDC| Host
|
||||
```
|
||||
|
||||
The signer speaks the same algorithm-based API as the host and the CYD/Teensy
|
||||
firmware ([`README.md`](../../README.md) §4). The auth envelope (kind 27235)
|
||||
protects the IR wire. The receiver dongle is a transparent byte pipe — it has
|
||||
no knowledge of the protocol, no keys, and no state beyond the IR-to-USB
|
||||
bridge.
|
||||
|
||||
## Why IR
|
||||
|
||||
- **True air-gap** — the signer is electrically isolated from the host. No wire,
|
||||
no radio, no shared ground. Host-side malware cannot reach the signer's
|
||||
firmware through the communication channel.
|
||||
- **Line-of-sight required** — you point the signer at the receiver. An attacker
|
||||
would need to be in the same room, in the line of sight, with their own IR
|
||||
transmitter. Much smaller attack surface than BT (which broadcasts
|
||||
omnidirectionally to ~10 m).
|
||||
- **Dumb dongle** — the USB receiver is a simple IR-to-serial bridge. ~200
|
||||
lines of firmware, no crypto, no keys, fully auditable in an afternoon. If
|
||||
compromised, it can only MITM the IR stream (which is already protected by the
|
||||
auth envelope).
|
||||
- **No BT stack** — much smaller firmware attack surface on the signer. No
|
||||
pairing, no GATT, no L2CAP, no SMP.
|
||||
- **Novel** — no hardware wallet uses IR for host communication. It's a
|
||||
creative solution to the air-gap problem that avoids both the wire (USB) and
|
||||
the radio (BT/NFC) attack surfaces.
|
||||
|
||||
## Hardware (preliminary)
|
||||
|
||||
### Signer
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **RP2040** (Raspberry Pi Pico) | $4, Cortex-M0+ @ 133 MHz, 264 KB SRAM, no WiFi/BT (perfect for air-gap). Enough RAM for secp256k1 + ed25519. ML-DSA-65 fits (~6 KB heap). |
|
||||
| | or **nRF52840** | If you want NFC for mnemonic loading + lower power. |
|
||||
| IR transceiver | **38 kHz IR LED + TSOP38238** (raw async, 115200 baud, ~$1) | Simplest. ~11 KB/s. Fine for Nostr events (~500 bytes). Slow for PQ sigs (3-8 KB → 0.3-0.7 s). |
|
||||
| | or **TFBS4711 IrDA module** (~$2, up to 4 Mbps) | Faster (~400 KB/s) but harder to source + more complex protocol. |
|
||||
| Display | 0.96" SSD1306 OLED (I2C, ~$2) or 1.54" e-paper | Small is fine — shows "approve kind 1 from <caller>?" |
|
||||
| Input | 2-3 tactile buttons (approve/deny/back) | |
|
||||
| Power | Coin cell or small LiPo | RP2040 + OLED + IR = very low power |
|
||||
|
||||
### USB receiver dongle
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **RP2040** (Pico) or **ATmega32U4** (Arduino Micro) | $4-8. Native USB device. |
|
||||
| IR receiver | Matching TSOP38238 or IrDA module | Must match the signer's IR modulation. |
|
||||
| USB | Native USB CDC-ACM | Presents as `/dev/ttyACM0` to the host. |
|
||||
| Firmware | ~200 lines | Read IR → write USB CDC; read USB CDC → transmit IR. A dumb pipe. No crypto, no keys, no state. |
|
||||
|
||||
## Throughput
|
||||
|
||||
| IR mode | Baud | Throughput | sign_event (500 B req + 600 B resp) | ML-DSA-65 sign (3.3 KB sig) |
|
||||
|---|---|---|---|---|
|
||||
| Raw 38 kHz async | 115200 | ~11 KB/s | ~100 ms | ~300 ms |
|
||||
| Raw 38 kHz async | 230400 | ~23 KB/s | ~50 ms | ~150 ms |
|
||||
| IrDA | 4 Mbps | ~400 KB/s | ~3 ms | ~8 ms |
|
||||
|
||||
**Recommendation:** start with raw 38 kHz IR at 115200 baud (simplest, cheapest,
|
||||
works with any IR LED + TSOP receiver). Upgrade to 230400 or IrDA if PQ
|
||||
signature throughput is a bottleneck.
|
||||
|
||||
## Protocol
|
||||
|
||||
The IR link is **half-duplex** — the signer and receiver take turns
|
||||
transmitting. The protocol is simple:
|
||||
|
||||
1. Host sends JSON-RPC request → USB CDC → dongle transmits IR.
|
||||
2. Signer receives IR, parses the request, shows approval prompt.
|
||||
3. User approves/denies.
|
||||
4. Signer transmits IR response → dongle → USB CDC → host.
|
||||
|
||||
The 4-byte big-endian length-prefix framing (same as the CYD/feather) works
|
||||
over IR as-is. The auth envelope protects against MITM on the IR stream.
|
||||
|
||||
## Security model
|
||||
|
||||
- **Electrical isolation:** the signer has no electrical connection to the host.
|
||||
The IR link is a one-way-at-a-time optical channel.
|
||||
- **Line-of-sight:** an attacker must be in the same room, in the line of sight,
|
||||
with their own IR transmitter. The auth envelope + approval prompt protect
|
||||
against a MITM even if the attacker intercepts the IR stream.
|
||||
- **Dumb dongle:** the USB receiver has no crypto, no keys, no protocol
|
||||
knowledge. It's a byte pipe. If compromised, it can only MITM the IR stream
|
||||
(already protected by the auth envelope). The dongle's firmware is small
|
||||
enough to audit in an afternoon.
|
||||
- **No radio:** no BT, no WiFi, no NFC (unless you add NFC for mnemonic loading).
|
||||
The signer emits no RF — only modulated IR light when actively transmitting.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **IR modulation:** raw 38 kHz async (simplest) vs IrDA (faster, more complex)?
|
||||
- **Mnemonic entry:** buttons (scroll BIP-39 words) vs NFC from phone vs
|
||||
generate-on-device? On a 0.96" OLED, scrolling 2048 words is tedious but
|
||||
secure.
|
||||
- **PQ crypto on RP2040:** 264 KB SRAM is enough for ML-DSA-65 but SLH-DSA-128s
|
||||
is tight. May need to limit the PQ algorithm set.
|
||||
- **Dongle design:** separate RP2040 Pico, or integrate the IR receiver into a
|
||||
custom PCB with a USB-A plug for a compact dongle?
|
||||
- **Range:** raw IR with an IR LED + TSOP38238 reaches ~1-2 m line-of-sight.
|
||||
Enough for "point at the dongle on your desk" but not across a room.
|
||||
- **Bidirectional IR:** the signer needs both an IR LED (transmit) and a TSOP
|
||||
receiver (receive). Two modules, or an IrDA transceiver module that does both?
|
||||
|
||||
## Comparison to the BLE wearable signer
|
||||
|
||||
| | IR air-gap | BLE wearable |
|
||||
|---|---|---|
|
||||
| Air-gap | **High (light, line-of-sight, ~1 m)** | Medium (radio, ~10 m, omnidirectional) |
|
||||
| Attack surface | **Small (no BT, dumb dongle)** | Large (BT stack) |
|
||||
| Host compatibility | Requires USB dongle | Universal (phones, laptops) |
|
||||
| Form factor | Handheld (point at dongle) | Wearable |
|
||||
| Throughput | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) | ~250 KB/s (BLE 5) |
|
||||
| Novelty | **Novel (no hardware wallet uses IR)** | Conventional |
|
||||
| Cost | ~$10 signer + ~$8 dongle | ~$10-15 (nRF52840 + OLED) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide on IR modulation (raw 38 kHz vs IrDA)
|
||||
- Decide on MCU (RP2040 vs nRF52840)
|
||||
- Decide on mnemonic entry method
|
||||
- Decide on display (OLED vs e-paper)
|
||||
- Prototype the IR link: two RP2040 Picos + IR LEDs + TSOP38238, bidirectional
|
||||
byte pipe at 115200 baud
|
||||
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
@@ -0,0 +1,192 @@
|
||||
# n_signer NFC Card / Ring Signer
|
||||
|
||||
**Status:** Concept — open-ended brainstorming. No plan yet.
|
||||
|
||||
A contactless signer in a **card or ring form factor** that is powered and
|
||||
communicates via **NFC (13.56 MHz)**. You place it on a reader (USB NFC reader
|
||||
or a phone); the reader's RF field powers the device and exchanges data. No
|
||||
battery, no wire, no radio beyond the 4 cm NFC zone.
|
||||
|
||||
This directory also explores **passive RFID/NFC tag ideas** that don't do
|
||||
signing on-device — they store keys or seed material that a host reads and uses.
|
||||
|
||||
---
|
||||
|
||||
## Concept A: NFC-powered active signer (card with display + button)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: laptop/phone<br/>n_signer client] -->|USB or built-in NFC| Reader[NFC reader<br/>ACR122U or phone]
|
||||
Reader -->|13.56 MHz RF field<br/>powers + communicates| Card[Signer card<br/>nRF52840 + e-paper + button]
|
||||
Card -->|NFC response| Reader
|
||||
Reader -->|USB| Host
|
||||
```
|
||||
|
||||
The card has a tiny e-paper display + one button. The reader powers the card;
|
||||
the card shows the approval prompt on its own display; the user presses the
|
||||
button to approve; the card signs and sends the response over NFC. The card
|
||||
does not trust the reader for display — it shows what it's signing.
|
||||
|
||||
### Hardware (preliminary)
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **nRF52840** (WLCSP) | Cortex-M4, NFC-A tag mode built in, secp256k1/ed25519 in software. Needs a thin-film battery (not fully passive). |
|
||||
| | or **NXP JCOP 4** (Java Card) | Fully passive, hardware secp256k1, tamper-resistant. Requires NDA + Java Card applet. Not DIY. |
|
||||
| Display | 1.1" e-paper segment display | Shows "approve kind 1? caller: <hex>". Zero power when static. |
|
||||
| Input | One capacitive touch button | Press to approve, timeout = deny. |
|
||||
| Power | Thin-film battery (like payment cards with displays) + NFC harvesting | |
|
||||
| Antenna | Etched into flex PCB around card perimeter | Standard smart card manufacturing. |
|
||||
|
||||
### Security
|
||||
|
||||
- **Attack radius ~4 cm** — an attacker must touch your card with their reader.
|
||||
- **No emissions when not on a reader** — the card is invisible to remote attackers.
|
||||
- **Self-contained approval** — the card's display shows what it's signing. The reader can't lie.
|
||||
- **Physical possession = authorization** — same model as a payment card.
|
||||
|
||||
### Build difficulty
|
||||
|
||||
- **DIY prototype:** nRF52840 dev board + wire-wound NFC antenna + ACR122U reader + small OLED. Prove NFC-powered signing works.
|
||||
- **Production:** custom flex PCB + etched antenna + thin battery + e-paper segment. Standard smart card manufacturing, but not DIY.
|
||||
|
||||
---
|
||||
|
||||
## Concept B: NFC ring (tap-to-sign, no display)
|
||||
|
||||
A ring with an NFC tag + MCU inside. No display, no button. You tap it on a
|
||||
reader; the reader displays the approval prompt; you tap again to confirm
|
||||
(two-tap protocol) or the ring signs immediately (single-tap, trusts the reader).
|
||||
|
||||
### Hardware
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | Secure element (JCOP / Infineon) or nRF52840 (WLCSP) | Must be tiny (2×3 mm package). |
|
||||
| Power | **Fully passive** (harvested from reader) if using a secure element. nRF52840 needs a battery. |
|
||||
| Antenna | Coil wound into the ring body | Custom manufacturing. |
|
||||
| Display | **None** | No room. |
|
||||
| Input | **None** | Pure tap-to-sign. |
|
||||
|
||||
### Security
|
||||
|
||||
- **Two-tap protocol:** tap to receive the request, reader displays it, tap again to sign. Forces deliberate action but still trusts the reader's display.
|
||||
- **Single-tap:** anyone who taps your ring with a reader can sign. Only safe if the ring is always in your physical possession and the reader is trusted.
|
||||
- **No display = reader-trusted approval.** Weaker than Concept A but much more portable.
|
||||
|
||||
### Build difficulty
|
||||
|
||||
- **Very hard** — custom antenna winding, tiny chip placement, ring-form-factor PCB. Not DIY without specialized equipment.
|
||||
|
||||
---
|
||||
|
||||
## Concept C: Passive NFC tag that stores keys (no on-device signing)
|
||||
|
||||
This is a fundamentally different idea: the tag **does not sign anything**. It
|
||||
stores key material (a mnemonic seed, a private key, or a derived key) that a
|
||||
host reads over NFC and uses to sign. The tag is a **portable key storage
|
||||
device**, not a signer.
|
||||
|
||||
### How it would work
|
||||
|
||||
1. The user taps the tag on a phone or USB NFC reader.
|
||||
2. The host reads the stored key material over NFC (ISO 14443 / NDEF).
|
||||
3. The host uses the key material to sign (the host runs the n_signer crypto).
|
||||
4. The tag is just storage — it has no MCU, no crypto, no battery.
|
||||
|
||||
### What the tag stores (options)
|
||||
|
||||
| Storage model | What's on the tag | Security | Notes |
|
||||
|---|---|---|---|
|
||||
| **Encrypted seed** | The mnemonic seed, encrypted with a passphrase (BIP-39 password). The host decrypts after the user types the passphrase. | Medium — if the tag is stolen, the attacker needs the passphrase. | Like an encrypted paper backup, but in NFC form. |
|
||||
| **Raw private key** | The secp256k1 private key (32 bytes), stored in the tag's EEPROM. | **Low** — anyone who reads the tag has the key. Only safe if the tag is PIN-protected (needs a secure element, not a dumb tag). | Like storing a private key on a USB stick. |
|
||||
| **NDEF URI** | A URI like `nostr:npub1...` (just the public key). The host uses it to identify which key to use (the actual private key is elsewhere). | High (it's just a pubkey) | Not a signer — just an identity token. |
|
||||
| **Shamir shard** | One share of a Shamir's Secret Sharing split of the seed. The tag holds 1 of N shares; you need M tags to reconstruct. | **High** — a single tag is useless. | Like a metal seed backup but in NFC form. Multiple tags = multiple shares. |
|
||||
| **HD wallet derivation path** | Just the derivation path + a reference to a master seed stored elsewhere. The tag tells the host *which* key to derive. | Medium | The tag is a pointer, not the key itself. |
|
||||
|
||||
### Hardware
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| **NTAG215** (NXP) | 504 bytes user memory, no crypto, ~$0.10 | The cheapest option. Stores an encrypted seed or NDEF URI. No MCU. |
|
||||
| **NTAG424 DNA** (NXP) | 4 KB, AES-128, tamper detection, ~$0.50 | Has crypto — can do authenticated read (the host must present a key to read the data). Better security. |
|
||||
| **MIFARE DESFire EV3** | 32 KB, AES, secure applets, ~$1 | Smart card chip. Can store encrypted key material with PIN/mutual auth. |
|
||||
| **Java Card (JCOP)** | Full smart card, runs applets, ~$3-5 | Could run a "key storage" applet that only releases the seed after a PIN is verified on the host. |
|
||||
|
||||
### Security analysis
|
||||
|
||||
The **passive tag as key storage** is the weakest signer model (the host does
|
||||
the signing, so a compromised host can steal the key), but it has interesting
|
||||
niche uses:
|
||||
|
||||
- **Encrypted seed backup:** an NTAG215 storing an encrypted seed is a
|
||||
convenient portable backup — tap your phone to read it, type the passphrase
|
||||
to decrypt. More convenient than a metal plate, less secure than a hardware
|
||||
signer.
|
||||
- **Shamir shard carrier:** each tag holds one SSS share. You need M of N tags
|
||||
to reconstruct the seed. Distribute the tags to different locations/people.
|
||||
A single stolen tag is useless. This is a **key-recovery** tool, not a signer.
|
||||
- **Identity token:** an NDEF URI tag with your npub. Tap to share your Nostr
|
||||
identity with a phone. Not a signer — just a business card for Nostr.
|
||||
- **PIN-protected key release:** a DESFire or JCOP tag that only releases the
|
||||
seed after the host verifies a PIN. The host never sees the key until the PIN
|
||||
is correct. Better than a raw tag, but the host still gets the key after the
|
||||
PIN — so a compromised host can still steal it.
|
||||
|
||||
### The fundamental limitation
|
||||
|
||||
A passive tag that stores keys **cannot protect the key from a compromised
|
||||
host**. Once the host reads the key, the host has it. This is the same problem
|
||||
as storing a private key in a file — the OS can steal it. The only way to
|
||||
protect the key from the host is to **never release the raw key** — which means
|
||||
the tag must do the signing itself (Concept A or B), or the tag must participate
|
||||
in a protocol where the host sends a hash to sign and the tag returns a
|
||||
signature (which requires an MCU + crypto = not a passive tag).
|
||||
|
||||
**The one exception:** a **secure element** (JCOP / Infineon) can do
|
||||
"sign inside, never release the key." The host sends the message hash; the
|
||||
secure element signs it internally and returns the signature. The private key
|
||||
never leaves the chip. This is how smart card signing works (e.g. FIDO2 keys,
|
||||
PIV cards). But this is Concept A (active signer), not a passive tag.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (all concepts)
|
||||
|
||||
- **Is the goal a signer (does crypto on-device) or a key carrier (stores keys for a host to use)?**
|
||||
- Signer → Concept A (card with display) or B (ring). The key never leaves the device.
|
||||
- Key carrier → Concept C (passive tag). The host gets the key. Simpler but less secure.
|
||||
- **Form factor:** card (credit card size, room for display) vs ring (tiny, no display)?
|
||||
- **Power:** passive (secure element, no battery) vs semi-passive (nRF52840 + thin battery)?
|
||||
- **Approval model:** on-device display (secure, needs a screen) vs reader display (trusts the reader, no screen needed) vs two-tap (medium security, no screen)?
|
||||
- **Host reader:** USB NFC reader (ACR122U, ~$15) vs phone NFC (universal, no dongle)?
|
||||
- **MCU/toolchain:** nRF52840 + C (DIY-friendly) vs JCOP + Java Card (production, NDA)?
|
||||
- **Could a passive tag + a host-side n_signer be a useful "portable encrypted seed backup" even if it's not a signer?** (Yes — for key recovery / Shamir shard distribution.)
|
||||
|
||||
---
|
||||
|
||||
## Comparison across all three concepts (NFC, BLE, IR)
|
||||
|
||||
| | NFC card (A) | NFC ring (B) | NFC tag (C) | BLE wearable | IR air-gap |
|
||||
|---|---|---|---|---|---|
|
||||
| Does signing on-device? | **Yes** | **Yes** | No (host signs) | Yes | Yes |
|
||||
| Key leaves device? | **No** | **No** | Yes (host reads it) | No | No |
|
||||
| Power | Reader + thin battery | Reader (passive) or battery | **Reader (passive)** | Battery | Battery |
|
||||
| Display | Tiny e-paper | None | None | Tiny OLED | Tiny OLED/e-paper |
|
||||
| Approval | On-card display + button | Reader display (two-tap) | N/A (host decides) | On-device display + buttons | On-device display + buttons |
|
||||
| Attack radius | ~4 cm | ~4 cm | ~4 cm | ~10 m | ~1 m (line-of-sight) |
|
||||
| Host needs | NFC reader or phone NFC | NFC reader or phone NFC | NFC reader or phone NFC | BT (universal) | USB IR dongle |
|
||||
| Form factor | Card | Ring | Tag/card/sticker | Wristband/pendant | Handheld |
|
||||
| Build difficulty | Hard (flex PCB) | Very hard (custom ring) | **Easy (off-the-shelf tag)** | Moderate | Moderate |
|
||||
| Best for | Daily signing | Quick tap-to-sign | Key backup / recovery | Wearable daily use | High-security air-gap |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide: signer (A/B) vs key carrier (C) vs both
|
||||
- Decide: card vs ring vs tag
|
||||
- Decide: DIY prototype (nRF52840 + ACR122U) vs production (secure element)
|
||||
- Explore the Shamir-shard-on-NFC-tags idea as a key-recovery tool
|
||||
- Explore the encrypted-seed-on-NTAG215 idea as a portable backup
|
||||
- Write a plan for the chosen direction
|
||||
@@ -0,0 +1,241 @@
|
||||
# n_signer Teensy 4.1 Firmware
|
||||
|
||||
**Status:** In progress — Phase 0 + SD + TFT + touch all verified on hardware
|
||||
(toolchain, blink, USB CDC serial, 4-bit SDMMC card read, ST7796S 480×320 TFT
|
||||
landscape fill + text, XPT2046 touch calibrated + live dot-draw verified). See
|
||||
[`plans/teensy41_bringup.md`](../../plans/teensy41_bringup.md) for the bring-up
|
||||
log, [`firmware/teensy41/WIRING.md`](WIRING.md) for the display wiring, and
|
||||
[`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md) for the
|
||||
full port plan. Next: exFAT re-test on the 1 TB SDXC card, then port the CYD
|
||||
display/touch/UI code.
|
||||
|
||||
The **Teensy 4.1** (NXP i.MX RT1062, Cortex-M7 @ 600 MHz) is the high-capacity
|
||||
OTP pad target. Its built-in SD slot supports **1 TB SDXC cards (exFAT)** via
|
||||
PJRC's SdFat library, making it the ideal hardware signer for large one-time-pad
|
||||
storage.
|
||||
|
||||
## Why the Teensy 4.1
|
||||
|
||||
| Concern | Teensy 4.1 | CYD (ESP32) | Feather S3 TFT (ESP32-S3) |
|
||||
|---|---|---|---|
|
||||
| MCU | 600 MHz Cortex-M7 | 240 MHz Xtensa LX6 | 240 MHz Xtensa LX7 |
|
||||
| SRAM | 1 MB + 16 MB PSRAM | 512 KB (no PSRAM) | 512 KB + quad PSRAM |
|
||||
| SD slot | **4-bit SDMMC, exFAT, up to 2 TB** | 1-bit SDSPI, FAT32, up to 32 GB | — |
|
||||
| USB | Hi-Speed (480 Mbps) device + host | CH340 UART only | Full-Speed USB |
|
||||
| WiFi | **None** | Yes (unused) | Yes (unused) |
|
||||
| Ethernet | 10/100 PHY (optional) | — | — |
|
||||
| PQ crypto speed | ~1-2 s SLH-DSA-128s | 5-30 s SLH-DSA-128s | 5-30 s SLH-DSA-128s |
|
||||
|
||||
## SD card size support
|
||||
|
||||
| Card type | Size | Works? |
|
||||
|---|---|---|
|
||||
| SDSC | ≤ 2 GB | Yes (FAT16/32) |
|
||||
| SDHC | 2 – 32 GB | Yes (FAT32) |
|
||||
| **SDXC** | **32 GB – 2 TB** | **Yes (exFAT via SdFat)** |
|
||||
| SDUC | 2 – 128 TB | No |
|
||||
|
||||
**1 TB SDXC cards work** — SdFat has native exFAT support, and the Teensy's
|
||||
4-bit SDMMC bus runs at ~20-40 MB/s. No reformatting needed.
|
||||
|
||||
## Display + touch
|
||||
|
||||
**4.0" ST7796S 480×320 SPI TFT with XPT2046 resistive touch** (Hosyond or
|
||||
equivalent, ~$12-15). Specs: 4-wire SPI, RGB 65K, 3.3V~5V (works at the Teensy's
|
||||
3.3V logic), XPT2046 resistive touch, includes touch pen + on-module SD slot.
|
||||
|
||||
Resistive touch is the right choice for a hardware signer (deliberate physical
|
||||
activation, stylus-compatible, simple driver). The XPT2046 driver ports from
|
||||
the CYD's [`touch.c`](../cyd_esp32_2432s028/main/touch.c) with 480×320
|
||||
resolution constants. The ST7796S display driver is new code (different init
|
||||
sequence than the CYD's ILI9341).
|
||||
|
||||
Wiring: TFT SPI on pins 11/12/13 (hardware SPI0), CS=5, DC=7, RESET=6, BL=8;
|
||||
touch shares the SPI bus with T_CS=9, T_IRQ=2. See
|
||||
[`WIRING.md`](WIRING.md) for the full pin table and wire-by-wire chart.
|
||||
|
||||
## Calibrated values & settings (verified on hardware)
|
||||
|
||||
These are the values to use when porting the CYD display/touch/UI code to the
|
||||
Teensy 4.1. They were measured on the actual Elecrow 4.0" ST7796S + XPT2046
|
||||
module during bring-up (see [`plans/teensy41_bringup.md`](../../plans/teensy41_bringup.md)).
|
||||
|
||||
### Display
|
||||
|
||||
| Setting | Value | Notes |
|
||||
|---|---|---|
|
||||
| Orientation | **Landscape 480×320** | Wider than tall. |
|
||||
| `tft.init()` | `tft.init(320, 480)` | Pass the *native* (portrait) dims so the library takes its zero-offset branch. Do NOT use `init(480, 320)` — that computes a negative `_colstart` and offsets the image. |
|
||||
| `tft.setRotation()` | `setRotation(1)` | Produces landscape 480×320 from the `init(320, 480)` base. |
|
||||
| `SCREEN_W` / `SCREEN_H` | `480` / `320` | Use these constants for all drawing coordinates. **Do not use `tft.width()` / `tft.height()`** — the `ST7796_t3` accessors are broken and always return 480×320 regardless of rotation. |
|
||||
| Library | `ST7796_t3` (in Teensy's `ST7735_t3` library) | Hardware SPI0. |
|
||||
| `drawPixel()` | **Avoid near edges** | Use `fillRect(x, y, 1, n, c)` / `fillRect(x, y, n, 1, c)` for crosshairs and thin lines — `drawPixel` mispositions near the right/bottom edges. |
|
||||
|
||||
### Touch (XPT2046)
|
||||
|
||||
| Setting | Value | Notes |
|
||||
|---|---|---|
|
||||
| `T_CS` | pin 9 | Touch chip select (active low). |
|
||||
| `T_IRQ` | **pin 2** | Touch interrupt. **Must not be pin 10** — pin 10 is SPI0 CS0, and `pinMode(10, INPUT)` corrupts the SPI engine and reverts the display to the wrong orientation. |
|
||||
| SPI clock | 2 MHz | XPT2046 max is ~2.5 MHz; drop the clock before every touch read (`SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0))`). |
|
||||
| Axis swap | **Yes** | In landscape rotation 1, raw Y → screen X, raw X → screen Y. The calibration struct + `raw_to_screen()` handle this (see below). |
|
||||
| Control bytes | `0xD0` = X, `0x90` = Y, `0xB0`/`0xC0` = Z1/Z2 | Same as the CYD's [`touch.c`](../cyd_esp32_2432s028/main/touch.c). |
|
||||
| Pressure threshold | 80 | Reject readings with `pressure < 80` (stylus not pressing hard enough). Same as CYD. |
|
||||
|
||||
### Touch calibration constants (measured 2026-07-26)
|
||||
|
||||
```c
|
||||
static TouchCal s_cal = {
|
||||
/* x_min = */ 207,
|
||||
/* x_max = */ 1909,
|
||||
/* y_min = */ 168,
|
||||
/* y_max = */ 1798,
|
||||
/* invert_x = */ 1,
|
||||
/* invert_y = */ 1,
|
||||
};
|
||||
```
|
||||
|
||||
With the axis swap, `x_min/x_max` is the range of **raw Y** that maps to screen
|
||||
X, and `y_min/y_max` is the range of **raw X** that maps to screen Y. The
|
||||
`raw_to_screen()` function swaps accordingly:
|
||||
|
||||
```c
|
||||
*sx = map_clamped((int)raw_y, s_cal.x_min, s_cal.x_max,
|
||||
s_cal.invert_x ? (SCREEN_W - 1) : 0,
|
||||
s_cal.invert_x ? 0 : (SCREEN_W - 1));
|
||||
*sy = map_clamped((int)raw_x, s_cal.y_min, s_cal.y_max,
|
||||
s_cal.invert_y ? (SCREEN_H - 1) : 0,
|
||||
s_cal.invert_y ? 0 : (SCREEN_H - 1));
|
||||
```
|
||||
|
||||
These constants are for the specific panel we calibrated. If you swap a
|
||||
display module, re-run [`firmware/teensy41/touch_cal/touch_cal.ino`](touch_cal/touch_cal.ino)
|
||||
and paste the new constants.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Arduino CLI / Teensyduino — uses the custom linker script
|
||||
bash firmware/teensy41/build_signer.sh # compile only
|
||||
bash firmware/teensy41/build_signer.sh --flash # compile + upload
|
||||
bash firmware/teensy41/build_signer.sh --test # compile + upload + run tests
|
||||
```
|
||||
|
||||
The build uses a custom linker script
|
||||
([`signer/imxrt1062_t41_flashmem.ld`](signer/imxrt1062_t41_flashmem.ld)) that
|
||||
routes crypto and SdFat code to FLASH (off-chip QSPI) instead of ITCM
|
||||
(tightly-coupled RAM), freeing the limited FlexRAM for stack. See the memory
|
||||
management section below for details.
|
||||
|
||||
See the port plan: [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md).
|
||||
|
||||
## Memory management
|
||||
|
||||
The Teensy 4.1's NXP i.MX RT1062 has a unique FlexRAM architecture that makes
|
||||
memory budgeting the central engineering challenge of this firmware. This
|
||||
section documents the layout, the custom linker script, and the build-time
|
||||
stack gauge that catches overflows before they become mystery crashes.
|
||||
|
||||
### The FlexRAM problem
|
||||
|
||||
The i.MX RT1062 has three RAM regions:
|
||||
|
||||
```
|
||||
FLASH 8 MB (off-chip QSPI, @ 0x60000000) — code + rodata, slow but huge
|
||||
FLEXRAM 512 KB (on-chip, split ITCM/DTCM) — fast, tiny, THE BOTTLENECK
|
||||
RAM2 512 KB (OCRAM, @ 0x20200000) — DMAMEM statics + malloc heap
|
||||
ERAM 0 MB (PSRAM pads empty) — not populated on the Teensy 4.1
|
||||
```
|
||||
|
||||
The 512 KB of FlexRAM is divided into **16 banks of 32 KB** that are split
|
||||
between **ITCM** (instruction tightly-coupled memory, runs code at zero wait
|
||||
state) and **DTCM** (data tightly-coupled memory, holds `.data` + `.bss` +
|
||||
**the stack**). The split is computed at **boot time** by the Teensy boot ROM
|
||||
from a formula in the linker script:
|
||||
|
||||
```ld
|
||||
_itcm_block_count = (SIZEOF(.text.itcm) + SIZEOF(.ARM.exidx) + 0x7FFF) >> 15;
|
||||
_estack = ORIGIN(DTCM) + ((16 - _itcm_block_count) << 15);
|
||||
```
|
||||
|
||||
**Every 32 KB bank given to code is taken away from the stack.** If ITCM code
|
||||
grows past a 32 KB boundary, a whole bank is stolen from DTCM, and the stack
|
||||
shrinks by 32 KB. The linker cannot detect this because the split happens at
|
||||
reset, not link time — an over-committed DTCM links cleanly and then hard-faults
|
||||
on boot.
|
||||
|
||||
### The custom linker script
|
||||
|
||||
[`signer/imxrt1062_t41_flashmem.ld`](signer/imxrt1062_t41_flashmem.ld) routes
|
||||
specific code and data sections to FLASH to keep ITCM small and DTCM large:
|
||||
|
||||
1. **Crypto code → FLASH**: secp256k1, ed25519, x25519, PQClean (ML-DSA-65,
|
||||
ML-KEM-768, SLH-DSA-128s), nostr_utils — all routed via per-object-file
|
||||
rules (`*secp256k1.c.o(.text*)`, etc.). These run slightly slower from
|
||||
FLASH but the stack headroom is the critical constraint.
|
||||
|
||||
2. **SdFat library → FLASH**: the FAT filesystem layer (FatFile, FatPartition,
|
||||
FatVolume, etc.) is routed to FLASH. The SDIO driver (SdioCard, SdioTeensy)
|
||||
stays in ITCM for fast interrupt response.
|
||||
|
||||
3. **`.rodata` → FLASH** (v0.1.6): read-only data (const tables, string
|
||||
literals, BIP-39 wordlist, LVGL fonts, PQClean constants) is routed to
|
||||
FLASH via `*(EXCLUDE_FILE(*ed25519.c.o) .rodata*)`. This reclaimed **124 KB
|
||||
of DTCM** (`.data` went from 131 KB to 6.8 KB), increasing free stack from
|
||||
5,984 bytes to **130,912 bytes**.
|
||||
|
||||
The `EXCLUDE_FILE(*ed25519.c.o)` exception is critical: the ed25519 base
|
||||
point constants (`ed_K`, `ed_X`, `ed_Y`) must stay in DTCM — moving them to
|
||||
FLASH produces an all-zeros pubkey (a regression documented in the linker
|
||||
script comments).
|
||||
|
||||
### Build-time stack gauge
|
||||
|
||||
[`check_stack.sh`](check_stack.sh) parses the ELF's section sizes after
|
||||
compilation and computes the same ITCM/DTCM split the boot ROM will perform.
|
||||
It **fails the build** if free stack would be below 16 KB:
|
||||
|
||||
```
|
||||
=== Teensy 4.1 FlexRAM stack gauge ===
|
||||
.text.itcm : 355056 bytes -> 11 banks (360448 bytes)
|
||||
.data (DTCM) : 6848 bytes
|
||||
.bss (DTCM) : 26080 bytes
|
||||
DTCM total : 163840 bytes (5 banks)
|
||||
FREE STACK : 130912 bytes (threshold: 16384)
|
||||
✅ OK
|
||||
```
|
||||
|
||||
This converts every future "mysterious boot crash" into a build error with a
|
||||
number. The gauge is wired into [`build_signer.sh`](build_signer.sh) and runs
|
||||
automatically after every compile.
|
||||
|
||||
### Current memory layout (v0.1.6)
|
||||
|
||||
```
|
||||
FLEXRAM 512 KB — 16 banks
|
||||
┌──────────────────────────────────────────────┬────────────────────────────────────────┐
|
||||
│ ITCM 11 banks = 352 KB │ DTCM 5 banks = 160 KB │
|
||||
│ code: 355 KB (signer + SDIO driver) │ .data: 6.8 KB (writable globals) │
|
||||
│ │ .bss: 25.4 KB (zero-init globals) │
|
||||
│ │ FREE STACK: 130.9 KB ✅ │
|
||||
└──────────────────────────────────────────────┴────────────────────────────────────────┘
|
||||
|
||||
RAM2 / OCRAM 512 KB
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ .bss.dma: 413.6 KB (LVGL draw buffers, crypto DMAMEM workspaces) heap: 110.7 KB free │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
FLASH 7936 KB
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Used: ~1.6 MB (crypto code + SdFat + .rodata + ITCM/DTCM load images) Free: ~6.3 MB │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### History
|
||||
|
||||
The memory budget was the direct cause of six versions of crash-fixing
|
||||
(v0.1.1–v0.1.6). The v0.1.6 `.rodata` → FLASH move was the largest single
|
||||
improvement, and it also enabled the SD-card OTP pad (which requires SdFat,
|
||||
adding ~7 KB of ITCM code that would have overflowed DTCM without the rodata
|
||||
reclamation). See [`plans/teensy41_memory_evaluation.md`](../../plans/teensy41_memory_evaluation.md)
|
||||
for the full analysis.
|
||||
@@ -0,0 +1,306 @@
|
||||
# Wiring: 4.0" ST7796S 480×320 SPI TFT + XPT2046 Touch → Teensy 4.1
|
||||
|
||||
This document describes how to wire the **Elecrow/Hosyond 4.0" 480×320 SPI TFT
|
||||
module with ST7796S driver and XPT2046 resistive touch** to the **Teensy 4.1**.
|
||||
|
||||
It is the reference for Phase 1 of [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md)
|
||||
(display + touch bring-up). Source documents for the pinout live in
|
||||
[`firmware/teensy41/documents/`](documents/):
|
||||
|
||||
- [`ST7796S_Datasheet.pdf`](documents/ST7796S_Datasheet.pdf) — display controller
|
||||
- [`4.0inch_SPI_Module_User_Manual.pdf`](documents/4.0inch_SPI_Module_User_Manual.pdf) — module manual
|
||||
- [`4.0 Inch 480_320 SPI TFT LCD Module with ST7796 Driver_With Touch Function - Elecrow Wiki.html`](documents/4.0%20Inch%20480_320%20SPI%20TFT%20LCD%20Module%20with%20ST7796%20Driver_With%20Touch%20Function%20-%20Elecrow%20Wiki.html)
|
||||
- [`Teensy® 4.1.html`](documents/Teensy%C2%AE%204.1.html) — PJRC pinout reference
|
||||
|
||||
## Module pinout
|
||||
|
||||
The module breaks out 14 pins along one long edge. The labels below are the
|
||||
**silk-screen names on the actual Elecrow/Hosyond board** (read in order from
|
||||
one end of the header to the other). Alternate names in parentheses are the
|
||||
ones used in some other vendors' docs.
|
||||
|
||||
| # | Silkscreen | Alt name | Function |
|
||||
|----|------------|-------------|-----------------------------------|
|
||||
| 1 | t_irq | | Touch interrupt (active low) |
|
||||
| 2 | t_do | T_MISO | Touch SPI MISO (shared bus) |
|
||||
| 3 | t_din | T_MOSI | Touch SPI MOSI (shared bus) |
|
||||
| 4 | t_cs | | Touch chip select (active low) |
|
||||
| 5 | t_clk | T_SCK | Touch SPI clock (shared bus) |
|
||||
| 6 | sdo (miso) | SDO / MISO | TFT SPI MISO (shared with touch) |
|
||||
| 7 | led | BL / BKL | Backlight (high = on; PWM-able) |
|
||||
| 8 | sck | CLK / SCL | TFT SPI clock (shared with touch) |
|
||||
| 9 | sdi (mosi) | SDI / SDA | TFT SPI MOSI (shared with touch) |
|
||||
| 10 | dc/rs | DC / A0 | TFT data/command select |
|
||||
| 11 | reset | RST | TFT reset (active low) |
|
||||
| 12 | cs | LCD_CS | TFT chip select (active low) |
|
||||
| 13 | gnd | | Ground |
|
||||
| 14 | vcc | | Power (3.3V–5V) |
|
||||
|
||||
> The module has a **second SD card slot** on the PCB (separate from the
|
||||
> Teensy's built-in slot). Its pins are not broken out on the 14-pin header —
|
||||
> they go to the SD contacts on the back of the module. We **do not use** the
|
||||
> module's SD slot; the 1 TB OTP pad lives in the Teensy's built-in SDMMC slot
|
||||
> (4-bit bus, ~20–40 MB/s, exFAT). See [`README.md`](README.md) §SD card size support.
|
||||
|
||||
## Connection chart (wire-by-wire)
|
||||
|
||||
This is the simple "connect this Teensy pin to that module pin" list. **Power
|
||||
first, then signals.**
|
||||
|
||||
### By display header position (use this when the silkscreen is hidden)
|
||||
|
||||
When the display is plugged into a breadboard, you can't read the silkscreen.
|
||||
This table is indexed by **physical position on the display header**, counting
|
||||
from the end with `t_irq` (pin 1) to the end with `vcc` (pin 14). Hold the
|
||||
display with the header at the bottom and the screen facing you; pin 1 is the
|
||||
leftmost pin (the `t_irq` end), pin 14 is the rightmost (the `vcc` end).
|
||||
|
||||
| Display header pos | Silkscreen | → | Teensy 4.1 pin | Function |
|
||||
|---|---|---|---|---|
|
||||
| 1 | t_irq | → | 2 | Touch interrupt (active low) — see note below on why not pin 10 |
|
||||
| 2 | t_do | → | 12 | SPI0 MISO (shared) |
|
||||
| 3 | t_din | → | 11 | SPI0 MOSI (shared) |
|
||||
| 4 | t_cs | → | 9 | Touch chip select (active low) |
|
||||
| 5 | t_clk | → | 13 | SPI0 SCK (shared) — opposite edge |
|
||||
| 6 | sdo (miso) | → | 12 | SPI0 MISO (same wire as pos 2) |
|
||||
| 7 | led | → | 8 | Backlight (PWM-able) |
|
||||
| 8 | sck | → | 13 | SPI0 SCK (same wire as pos 5) |
|
||||
| 9 | sdi (mosi) | → | 11 | SPI0 MOSI (same wire as pos 3) |
|
||||
| 10 | dc/rs | → | 7 | TFT data/command |
|
||||
| 11 | reset | → | 6 | TFT reset (active low) |
|
||||
| 12 | cs | → | 5 | TFT chip select (software CS) |
|
||||
| 13 | gnd | → | GND | Ground (opposite edge, next to pin 13) |
|
||||
| 14 | vcc | → | 3V3 | 3.3V power (long edge, past pin 12) — **NOT 5V** |
|
||||
|
||||
**Shared pins (pos 2↔6, 3↔9, 5↔8):** these pairs are tied together on the
|
||||
display PCB, so you only need to wire **one of each pair** to the Teensy. The
|
||||
table shows both for completeness, but in practice you can wire pos 2, 3, 5
|
||||
and leave pos 6, 8, 9 floating (or wire them to the same Teensy pin —
|
||||
harmless). That brings it to **11 wires** total (8 unique signals + 3V3 + GND
|
||||
+ the 3 shared-pair duplicates you choose to wire).
|
||||
|
||||
### By Teensy pin number (for lookup at the Teensy)
|
||||
|
||||
Sorted by Teensy pin number for easy lookup at the Teensy end of the wires.
|
||||
|
||||
| Teensy 4.1 pin | → | Module silkscreen | What it does |
|
||||
|----------------|---|-------------------|--------------|
|
||||
| GND | → | gnd | Ground (GND is on the opposite edge, next to pin 13) |
|
||||
| 3V3 | → | vcc | 3.3V power (**not 5V**) — the 3V3 pin is on the long edge, just past pin 12 |
|
||||
| 2 | → | t_irq | Touch interrupt (active low) — **not pin 10** (see note below) |
|
||||
| 5 | → | cs | TFT chip select (software CS — see note below) |
|
||||
| 6 | → | reset | TFT reset (active low) |
|
||||
| 7 | → | dc/rs | TFT data/command |
|
||||
| 8 | → | led | Backlight (HIGH = on; PWM-able) |
|
||||
| 9 | → | t_cs | Touch chip select (active low) |
|
||||
| 11 | → | sdi (mosi) | SPI0 MOSI (also wires to `t_din`) |
|
||||
| 12 | → | sdo (miso) | SPI0 MISO (also wires to `t_do`) |
|
||||
| 13 | → | sck | SPI0 SCK (also wires to `t_clk`) — **pin 13 is on the opposite edge**, route via a short jumper |
|
||||
|
||||
That's **11 wires** total, all in the pins 5-13 range plus 3V3 and GND. The
|
||||
three shared SPI lines (`sdi (mosi)`, `sdo (miso)`, `sck`) each fan out to two
|
||||
module pins — see the next section for why and how.
|
||||
|
||||
> **Why `cs` is on pin 5 (software CS) and `t_irq` is on pin 2 (not pin 10):**
|
||||
> Pin 10 is the hardware CS0 for SPI0. **Do not use pin 10 for anything else** —
|
||||
> calling `pinMode(10, ...)` (e.g. for `t_irq` as an input) corrupts the SPI0
|
||||
> engine state and reverts the ST7796S display to landscape mode after
|
||||
> `setRotation()`. This is a hard-won lesson documented in
|
||||
> [`plans/teensy41_bringup.md`](../../plans/teensy41_bringup.md). So:
|
||||
> - `cs` is on pin 5 (software CS via `SPI.beginTransaction()`) — the speed
|
||||
> difference vs hardware CS0 is negligible for a signer UI.
|
||||
> - `t_irq` is on pin 2 (long edge, before pin 5) — keeps it off pin 10 and
|
||||
> out of the SPI0 CS0 conflict.
|
||||
> If you want hardware CS0 for the TFT, you can put `cs` on pin 10, but then
|
||||
> `t_irq` must stay on pin 2 (or be dropped entirely in favor of polling).
|
||||
|
||||
> **Shared-bus shortcut:** `sdi (mosi)` and `t_din` are the same net on the
|
||||
> module PCB, `sdo (miso)` and `t_do` are the same net, and `sck` and `t_clk`
|
||||
> are the same net. So for each shared pair you can either (a) wire the Teensy
|
||||
> pin to **both** module pins (harmless, just a Y-jumper), or (b) wire the
|
||||
> Teensy pin to **one** module pin and leave the other floating (it's tied
|
||||
> internally on the PCB). The connection chart above wires each Teensy SPI pin
|
||||
> to the TFT-side silkscreen; the touch-side silkscreen of each pair is tied to
|
||||
> the same net on the board.
|
||||
|
||||
## Teensy 4.1 pin assignments
|
||||
|
||||
The Teensy 4.1 has 3 hardware SPI ports. We use **SPI0** (the primary port with
|
||||
the FIFO) on pins 11/12/13. The TFT and the XPT2046 share this bus; each has its
|
||||
own CS. All pins are 3.3V — the module is 3.3V-logic compatible, so no level
|
||||
shifting is needed.
|
||||
|
||||
| Silkscreen | Teensy 4.1 pin | Teensy function | Notes |
|
||||
|------------|----------------|--------------------------|-------|
|
||||
| gnd | GND | Ground | GND is on the opposite edge, next to pin 13. |
|
||||
| vcc | 3V3 | 3.3V output | **Use 3.3V, not 5V/VIN.** The 3V3 pin is on the long edge, just past pin 12. The Teensy's 3.3V regulator can supply up to ~250 mA; the display + backlight draw ~80–120 mA, well within budget. |
|
||||
| cs | 5 | GPIO (software CS) | TFT chip select (active low). Software CS via `SPI.beginTransaction()` — see the note in the connection chart about why we didn't use hardware CS0 on pin 10. |
|
||||
| reset | 6 | GPIO | TFT reset. Drive low for >10 µs to reset; pull high (or set as OUTPUT HIGH) for normal operation. Can also be tied to a 10 kΩ pull-up and left floating if you don't need software reset. |
|
||||
| dc/rs | 7 | GPIO | TFT data/command. High = data (pixel/command param), Low = command. |
|
||||
| led | 8 | GPIO / PWM | Backlight. Drive HIGH for full brightness, or use `analogWrite(8, 0..255)` for dimming. Can also be tied to 3.3V through a ~100 Ω resistor for always-on. |
|
||||
| t_cs | 9 | GPIO | Touch chip select. Active low. Must be HIGH when talking to the TFT, LOW when reading the touch. |
|
||||
| t_irq | 2 | GPIO (input + interrupt) | Touch interrupt. Pulled high by the XPT2046 internally; goes low when touch pressure is detected. Optional — can be left unconnected and polled, but wiring it lets us sleep until a touch happens. **Must not be pin 10** (SPI0 CS0 — `pinMode(10, INPUT)` corrupts the SPI engine and reverts the display to landscape). |
|
||||
| sdi (mosi) | 11 | SPI0 MOSI | Shared with touch `t_din`. |
|
||||
| sdo (miso) | 12 | SPI0 MISO | Shared with touch `t_do`. The TFT rarely drives MISO (most ST7796S commands are write-only), but the XPT2046 reads return on this line. |
|
||||
| sck | 13 | SPI0 SCK | Shared with touch `t_clk`. **Pin 13 is on the opposite edge** of the Teensy (next to GND), so route it via a short jumper across the board. Pin 13 also drives the onboard orange LED — that's fine, the LED just blinks during SPI activity. |
|
||||
| t_clk | 13 | SPI0 SCK (shared) | Same wire as `sck`. |
|
||||
| t_do | 12 | SPI0 MISO (shared) | Same wire as `sdo (miso)`. |
|
||||
| t_din | 11 | SPI0 MOSI (shared) | Same wire as `sdi (mosi)`. |
|
||||
|
||||
### Pin map summary (Teensy side)
|
||||
|
||||
Wires, listed in the same order as the module's silkscreen header (pin 1 → 14):
|
||||
|
||||
```
|
||||
Teensy 4.1 Silkscreen Function
|
||||
----------- ----------- ------------------------------------------
|
||||
pin 2 t_irq Touch interrupt (active low — NOT pin 10, see note)
|
||||
pin 12 t_do Touch MISO (shared SPI0 MISO)
|
||||
pin 11 t_din Touch MOSI (shared SPI0 MOSI)
|
||||
pin 9 t_cs Touch chip select (active low)
|
||||
pin 13 t_clk Touch SCK (shared SPI0 SCK — opposite edge)
|
||||
pin 12 sdo (miso) TFT MISO (shared SPI0 MISO — same wire as t_do)
|
||||
pin 8 led Backlight (HIGH = on; analogWrite for dimming)
|
||||
pin 13 sck TFT SCK (shared SPI0 SCK — same wire as t_clk)
|
||||
pin 11 sdi (mosi) TFT MOSI (shared SPI0 MOSI — same wire as t_din)
|
||||
pin 7 dc/rs TFT data/command
|
||||
pin 6 reset TFT reset (active low)
|
||||
pin 5 cs TFT chip select (software CS)
|
||||
GND gnd Ground (opposite edge, next to pin 13)
|
||||
3V3 vcc Power (3.3V — NOT 5V; long edge, past pin 12)
|
||||
```
|
||||
|
||||
Shared-bus note: `t_do` and `sdo (miso)` are the **same electrical net** on the
|
||||
module (both connect to the SPI MISO line), and `t_din`/`sdi (mosi)` and
|
||||
`t_clk`/`sck` are likewise the same nets. You only need to wire one of each
|
||||
shared pair to the Teensy — but on this module the header brings both out, so
|
||||
you can either wire both to the same Teensy pin (harmless) or wire one and leave
|
||||
the other floating (also fine, since they're tied together on the PCB). The
|
||||
table above wires both to the same Teensy pin for clarity.
|
||||
|
||||
## SPI bus sharing
|
||||
|
||||
The TFT and the XPT2046 share MOSI / SCK / MISO. The rule for sharing:
|
||||
|
||||
1. **Only one CS is low at a time.** Before talking to the TFT, set `t_cs` HIGH
|
||||
and `cs` LOW. Before reading the touch, set `cs` HIGH and `t_cs` LOW.
|
||||
2. **Use `SPI.beginTransaction(SPISettings(spi_speed, MSBFIRST, SPI_MODE0))` /
|
||||
`SPI.endTransaction()`** around each device's access. The TFT and the XPT2046
|
||||
both use SPI mode 0, but they may want different clock speeds:
|
||||
- TFT: up to 40 MHz (the ST7796S supports 1-line SPI up to ~62 MHz; 40 MHz is
|
||||
a safe conservative choice on the Teensy's SPI0).
|
||||
- XPT2046: 2–2.5 MHz max (the XPT2046 datasheet specifies a max serial clock
|
||||
of ~2.5 MHz). **Always drop the clock before reading the touch.**
|
||||
3. **The XPT2046 control byte** is `0x90` for X read (channel 5) and `0xD0` for
|
||||
Y read (channel 1), 8-bit, MSB first, with the start bit set. See
|
||||
[`firmware/cyd_esp32_2432s028/main/touch.c`](../cyd_esp32_2432s028/main/touch.c)
|
||||
`xpt2046_transfer_12b()` for the bit-banged reference; on the Teensy we can
|
||||
use the hardware SPI engine instead of bit-banging.
|
||||
|
||||
## Routing notes (trace-crossing analysis)
|
||||
|
||||
This assignment uses **hardware SPI0** (MOSI=11, MISO=12, SCK=13) for maximum
|
||||
display throughput (FIFO + DMA, up to 40 MHz), with all display pins in a
|
||||
contiguous block on the Teensy's long edge (pins 5-12) plus SCK=13 on the
|
||||
opposite edge. The display module's silkscreen pin order doesn't match the
|
||||
Teensy's SPI0 pin order, so a small number of trace crossings are unavoidable.
|
||||
This is fine for jumper wires and trivial on a 2-layer PCB.
|
||||
|
||||
**Teensy 4.1 physical layout (relevant part):**
|
||||
- **Long edge:** 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 3V3, 24, 25, ...
|
||||
- **Opposite edge:** 0, 13, 14, ... with GND in the middle
|
||||
|
||||
So pins 5-12 are contiguous on the long edge, 3V3 is right past pin 12, and
|
||||
pin 13 (SCK) + GND are on the opposite edge. All display signals fit in the
|
||||
pins 5-13 range plus 3V3 and GND — no need to reach up to pins 24+.
|
||||
|
||||
**Crossings (2 on the long edge + 1 SCK via):**
|
||||
|
||||
1. **MOSI/MISO swap.** Display order is MISO(`t_do`/`sdo`, pos 2/6) then
|
||||
MOSI(`t_din`/`sdi`, pos 3/9); Teensy SPI0 is MOSI(11) then MISO(12). One
|
||||
crossing — unavoidable given the fixed SPI0 pin assignments.
|
||||
2. **`t_cs` jumps over the SPI pins.** Display pos 3-4-5 is
|
||||
MOSI-`t_cs`-SCK; Teensy pins 11-12-13 are MOSI-MISO-SCK with no free pin
|
||||
between MOSI(11) and SCK(13). `t_cs` (Teensy pin 9) routes below the SPI
|
||||
block, crossing the MOSI/MISO pair. One crossing.
|
||||
3. **SCK via to the opposite edge.** SCK is on pin 13, which is on the
|
||||
opposite edge of the Teensy from pins 5-12. This is a via (a short jumper
|
||||
across the board), not a trace crossing on the long edge.
|
||||
|
||||
The remaining GPIOs (`led`=8, `dc/rs`=7, `reset`=6, `cs`=5) run monotonically
|
||||
down the long edge below `t_cs`=9 — zero crossings among them.
|
||||
|
||||
**Why not zero crossings?** A zero-crossing assignment is possible by
|
||||
bit-banging SPI on Teensy pins in exact display order, but it loses the
|
||||
hardware SPI engine — display refresh drops to ~0.2-0.5 s per full 480×320
|
||||
frame. We chose hardware SPI0 for a snappy UI and accepted 2 crossings + 1
|
||||
via, which is trivial for jumper wires or a 2-layer PCB.
|
||||
|
||||
**Why `cs`=5 (software CS) and `t_irq`=2?** Pin 10 is the hardware CS0 for
|
||||
SPI0, but **pin 10 cannot be used for `t_irq`** — calling `pinMode(10, INPUT)`
|
||||
corrupts the SPI0 engine and reverts the display to landscape after
|
||||
`setRotation()` (verified during bring-up, see
|
||||
[`plans/teensy41_bringup.md`](../../plans/teensy41_bringup.md)). So `t_irq` is
|
||||
on pin 2 (long edge, before pin 5), and `cs` is on pin 5 (software CS via
|
||||
`SPI.beginTransaction()`). The speed difference vs hardware CS0 is negligible
|
||||
for a signer UI. If you want hardware CS0 for the TFT, put `cs` on pin 10 and
|
||||
leave `t_irq` on pin 2 (or drop `t_irq` and poll).
|
||||
|
||||
## Power notes
|
||||
|
||||
- **3.3V only.** The Teensy 4.1's pins are **not 5V tolerant**. The display
|
||||
module is rated 3.3V–5V and works at 3.3V logic — power it from the Teensy's
|
||||
`3V3` pin, **not** `VIN`/`VUSB` (5V). Driving VCC with 5V while the signal
|
||||
pins are at 3.3V can back-power the ST7796S level shifters and is unnecessary.
|
||||
- **Backlight current.** The LED pin on these modules is typically driven
|
||||
through a transistor on the module; a direct 3.3V GPIO high is enough to turn
|
||||
it on. If the backlight flickers or the GPIO can't hold high, drive `LED` from
|
||||
3.3V through a ~100 Ω resistor instead and skip PWM dimming.
|
||||
- **Total draw.** Display + backlight + touch ≈ 80–120 mA at 3.3V. The Teensy's
|
||||
onboard regulator is rated for ~250 mA external use, so this is within budget.
|
||||
If you add other peripherals (Ethernet PHY, etc.), re-check the budget.
|
||||
|
||||
## Wiring checklist (do this before powering on)
|
||||
|
||||
- [ ] VCC → **3V3** (not 5V/VIN).
|
||||
- [ ] GND → GND.
|
||||
- [ ] All SPI signal pins (`cs`, `dc/rs`, `reset`, `sdi (mosi)`, `sck`,
|
||||
`sdo (miso)`, `t_cs`, `t_irq`) go to the Teensy pins listed above —
|
||||
confirm none are swapped. The easy one to get backwards is
|
||||
`sdi (mosi)` ↔ `sdo (miso)`: on this module **`sdi (mosi)` = Teensy pin 11
|
||||
(MOSI)** and **`sdo (miso)` = Teensy pin 12 (MISO)**.
|
||||
- [ ] The shared-bus pairs (`t_do`/`sdo (miso)`, `t_din`/`sdi (mosi)`,
|
||||
`t_clk`/`sck`) are tied together on the module PCB — wire one of each
|
||||
pair to the Teensy pin and the other can go to the same pin or float.
|
||||
Don't wire them to *different* Teensy pins.
|
||||
- [ ] No 5V signal reaches any Teensy pin.
|
||||
- [ ] The Teensy's built-in SD slot is **not** wired to the display — the
|
||||
module's SD slot is unused. The 1 TB pad goes in the Teensy's built-in
|
||||
slot only.
|
||||
|
||||
## Bring-up order (Phase 1)
|
||||
|
||||
1. **TFT only first.** Wire `vcc`, `gnd`, `cs`, `reset`, `dc/rs`, `sdi (mosi)`,
|
||||
`sck`, `led`, `sdo (miso)`. Leave `t_cs`, `t_irq` floating for now. Run a
|
||||
fill-screen + draw-text sketch. Exit criterion: solid color fill + readable
|
||||
text.
|
||||
2. **Touch second.** Add `t_cs`, `t_clk` (shared with `sck`), `t_din` (shared
|
||||
with `sdi (mosi)`), `t_do` (shared with `sdo (miso)`), `t_irq`. Run a
|
||||
touch-read sketch that prints raw X/Y + mapped pixel coords. Exit criterion:
|
||||
stylus press prints coordinates that track the stylus position across the
|
||||
full 480×320 area.
|
||||
3. **Calibration.** The XPT2046 raw readings need a 2-point calibration per
|
||||
axis (min/max raw → 0/480 and 0/320). Port the calibration struct from
|
||||
[`firmware/cyd_esp32_2432s028/main/touch.c`](../cyd_esp32_2432s028/main/touch.c)
|
||||
`s_cal` and re-calibrate on the Teensy (the CYD's numbers won't match —
|
||||
different panel, different controller).
|
||||
|
||||
## References
|
||||
|
||||
- Port plan: [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md)
|
||||
- Bring-up log: [`plans/teensy41_bringup.md`](../../plans/teensy41_bringup.md)
|
||||
- CYD display driver to port: [`firmware/cyd_esp32_2432s028/main/ili9341.c`](../cyd_esp32_2432s028/main/ili9341.c)
|
||||
- CYD touch driver to port: [`firmware/cyd_esp32_2432s028/main/touch.c`](../cyd_esp32_2432s028/main/touch.c)
|
||||
- ST7796S datasheet: [`firmware/teensy41/documents/ST7796S_Datasheet.pdf`](documents/ST7796S_Datasheet.pdf)
|
||||
- Module user manual: [`firmware/teensy41/documents/4.0inch_SPI_Module_User_Manual.pdf`](documents/4.0inch_SPI_Module_User_Manual.pdf)
|
||||
- Teensy 4.1 pinout: [`firmware/teensy41/documents/Teensy® 4.1.html`](documents/Teensy%C2%AE%204.1.html)
|
||||
@@ -0,0 +1,24 @@
|
||||
// Teensy 4.1 bring-up: blink the onboard red LED on pin 13 at 1 Hz.
|
||||
//
|
||||
// Phase 0 of plans/teensy41_signer_port.md. First sketch to load after the
|
||||
// board's application flash was erased (board enumerates as Halfkay bootloader
|
||||
// 16c0:0478 with no /dev/ttyACMx and no LED activity). A successful upload +
|
||||
// visible 1 Hz blink confirms the toolchain, the FQBN, and the USB upload path.
|
||||
//
|
||||
// Build / upload:
|
||||
// arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41/blink
|
||||
// arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41/blink
|
||||
//
|
||||
// Exit criterion: the red LED near the USB connector toggles at exactly 1 Hz
|
||||
// (slower than the PJRC factory blink, which is ~5 Hz, so the change is obvious).
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT); // LED_BUILTIN == 13 on Teensy 4.1
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
delay(500);
|
||||
digitalWrite(LED_BUILTIN, LOW);
|
||||
delay(500);
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# build_signer.sh — reproducible build + optional flash for the Teensy 4.1 n_signer.
|
||||
#
|
||||
# Usage:
|
||||
# ./firmware/teensy41/build_signer.sh # compile only
|
||||
# ./firmware/teensy41/build_signer.sh --flash # compile + upload
|
||||
# ./firmware/teensy41/build_signer.sh --test # compile + upload + run test suite
|
||||
#
|
||||
# This script handles:
|
||||
# - Copying lv_conf.h to the Arduino libraries directory (where LVGL's
|
||||
# lv_conf_internal.h searches for it via ../../lv_conf.h).
|
||||
# - Passing the custom linker script (imxrt1062_t41_flashmem.ld) that routes
|
||||
# crypto code to FLASH instead of ITCM, freeing stack space.
|
||||
# - Printing the memory usage report.
|
||||
# - Optionally uploading to the first discovered Teensy port.
|
||||
# - Optionally running the hardware test suite.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SIGNER_DIR="$SCRIPT_DIR/signer"
|
||||
FQBN="teensy:avr:teensy41"
|
||||
LV_CONF_SRC="$SIGNER_DIR/lv_conf.h"
|
||||
LV_CONF_DST="$HOME/Arduino/libraries/lv_conf.h"
|
||||
LINKER_SCRIPT="$SIGNER_DIR/imxrt1062_t41_flashmem.ld"
|
||||
|
||||
# --- Step 1: Copy lv_conf.h to the Arduino libraries directory ---
|
||||
if [ -f "$LV_CONF_SRC" ]; then
|
||||
mkdir -p "$(dirname "$LV_CONF_DST")"
|
||||
cp "$LV_CONF_SRC" "$LV_CONF_DST"
|
||||
echo "[build] Copied lv_conf.h to $LV_CONF_DST"
|
||||
else
|
||||
echo "[build] WARNING: $LV_CONF_SRC not found — LVGL may use default config."
|
||||
fi
|
||||
|
||||
# --- Step 2: Compile with the custom linker script ---
|
||||
echo "[build] Compiling..."
|
||||
LINKER_FLAG="-Wl,--gc-sections,--relax,--no-warn-rwx-segments -T${LINKER_SCRIPT}"
|
||||
|
||||
if [ -f "$LINKER_SCRIPT" ]; then
|
||||
arduino-cli compile \
|
||||
--fqbn "$FQBN" \
|
||||
--build-property "build.flags.ld=${LINKER_FLAG}" \
|
||||
"$SIGNER_DIR" || true # size-determination step may error with custom .ld
|
||||
else
|
||||
echo "[build] WARNING: Linker script not found at $LINKER_SCRIPT — using default."
|
||||
arduino-cli compile --fqbn "$FQBN" "$SIGNER_DIR"
|
||||
fi
|
||||
|
||||
# --- Step 2b: FlexRAM stack gauge ---
|
||||
# arduino-cli's "Error while determining sketch size" with a custom linker
|
||||
# script is non-fatal (the ELF is produced), but it means we lose the built-in
|
||||
# memory report. Run our own gauge against the ELF to catch DTCM stack
|
||||
# overflows that the linker cannot detect (the ITCM/DTCM split is computed at
|
||||
# boot, not link time).
|
||||
ELF_FILE=$(find "$HOME/.cache/arduino/sketches" -name "signer.ino.elf" -newer "$LINKER_SCRIPT" 2>/dev/null | head -1)
|
||||
if [ -z "$ELF_FILE" ]; then
|
||||
ELF_FILE=$(find "$HOME/.cache/arduino/sketches" -name "signer.ino.elf" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -n "$ELF_FILE" ] && [ -f "$ELF_FILE" ]; then
|
||||
echo "[build] Running FlexRAM stack gauge..."
|
||||
bash "$SCRIPT_DIR/check_stack.sh" "$ELF_FILE" 16384 || {
|
||||
echo "[build] ❌ Stack gauge FAILED — refusing to flash. See check_stack.sh output above."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "[build] WARNING: could not find signer.ino.elf for stack gauge — skipping."
|
||||
fi
|
||||
|
||||
echo "[build] Compilation successful."
|
||||
|
||||
# --- Step 3: Find the Teensy port ---
|
||||
find_teensy_port() {
|
||||
arduino-cli board list 2>/dev/null | grep 'teensy Teensy Ports' | awk '{print $1}' | head -1
|
||||
}
|
||||
|
||||
# --- Step 4: Flash (if --flash or --test) ---
|
||||
if [ "$1" = "--flash" ] || [ "$1" = "--test" ]; then
|
||||
PORT=$(find_teensy_port)
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "[flash] No Teensy found. Press the PROGRAM button on the Teensy."
|
||||
exit 1
|
||||
fi
|
||||
echo "[flash] Uploading to $PORT..."
|
||||
arduino-cli upload -p "$PORT" --fqbn "$FQBN" "$SIGNER_DIR"
|
||||
echo "[flash] Upload complete."
|
||||
fi
|
||||
|
||||
# --- Step 5: Run test suite (if --test) ---
|
||||
if [ "$1" = "--test" ]; then
|
||||
echo "[test] Waiting for device to boot..."
|
||||
sleep 4
|
||||
PORT_DEV=$(arduino-cli board list 2>/dev/null | grep 'ttyACM' | awk '{print $1}' | head -1)
|
||||
if [ -z "$PORT_DEV" ]; then
|
||||
PORT_DEV="/dev/ttyACM0"
|
||||
fi
|
||||
echo "[test] Running test suite on $PORT_DEV..."
|
||||
python3 "$SCRIPT_DIR/test_signer.py" --port "$PORT_DEV" || true
|
||||
fi
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# check_stack.sh — Teensy 4.1 FlexRAM stack gauge.
|
||||
#
|
||||
# Computes the same ITCM/DTCM split the i.MX RT1062 boot ROM will perform, and
|
||||
# fails if the resulting free stack is below a threshold. The linker cannot
|
||||
# catch this because the split is computed at reset from _itcm_block_count,
|
||||
# not at link time.
|
||||
#
|
||||
# Usage: check_stack.sh <signer.ino.elf> [min_stack_bytes]
|
||||
# min_stack_bytes defaults to 16384 (16 KB).
|
||||
#
|
||||
# Exits 0 if the stack is above the threshold, 1 if below (with a diagnostic).
|
||||
#
|
||||
# The arithmetic (from imxrt1062_t41_flashmem.ld:215-217):
|
||||
# itcm_banks = ceil((.text.itcm + .ARM.exidx) / 32768)
|
||||
# dtcm_total = (16 - itcm_banks) * 32768
|
||||
# free_stack = dtcm_total - .data - .bss
|
||||
# _estack = ORIGIN(DTCM) + dtcm_total (stack grows down from here)
|
||||
#
|
||||
# Note: .bss here is the DTCM .bss (RAM1), NOT .bss.dma (RAM2/OCRAM). The
|
||||
# .bss.dma section lives in a separate 512 KB region and does not affect the
|
||||
# stack.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ELF="${1:-}"
|
||||
MIN_STACK="${2:-16384}"
|
||||
|
||||
if [ -z "$ELF" ] || [ ! -f "$ELF" ]; then
|
||||
echo "check_stack: usage: $0 <signer.ino.elf> [min_stack_bytes]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Locate the arm-none-eabi-size tool from the Teensy toolchain.
|
||||
SIZE_BIN=""
|
||||
for candidate in \
|
||||
/home/user/.arduino15/packages/teensy/tools/teensy-compile/*/arm/bin/arm-none-eabi-size \
|
||||
"$(dirname "$0")/../.arduino15/packages/teensy/tools/teensy-compile/*/arm/bin/arm-none-eabi-size"; do
|
||||
if [ -x "$candidate" ]; then SIZE_BIN="$candidate"; break; fi
|
||||
done
|
||||
# Fall back to PATH
|
||||
if [ -z "$SIZE_BIN" ]; then SIZE_BIN="$(command -v arm-none-eabi-size || true)"; fi
|
||||
if [ -z "$SIZE_BIN" ]; then
|
||||
echo "check_stack: cannot find arm-none-eabi-size" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# We need per-section sizes, not the aggregate. Use objdump -h.
|
||||
OBJDUMP_BIN="${SIZE_BIN%size}objdump"
|
||||
|
||||
# Extract section sizes (in bytes, decimal) by name.
|
||||
get_section_size() {
|
||||
local section="$1"
|
||||
# objdump -h prints: idx Name Size VMA LMA File-off Al
|
||||
"$OBJDUMP_BIN" -h "$ELF" 2>/dev/null \
|
||||
| awk -v sec="$section" '$2 == sec { print strtonum("0x"$3); exit }'
|
||||
}
|
||||
|
||||
TEXT_ITCM=$(get_section_size ".text.itcm")
|
||||
ARM_EXIDX=$(get_section_size ".ARM.exidx")
|
||||
DATA_SEC=$(get_section_size ".data")
|
||||
BSS_SEC=$(get_section_size ".bss")
|
||||
BSS_DMA=$(get_section_size ".bss.dma")
|
||||
|
||||
# Guard against missing sections (e.g. exidx may be 0/absent).
|
||||
: "${TEXT_ITCM:=0}"
|
||||
: "${ARM_EXIDX:=0}"
|
||||
: "${DATA_SEC:=0}"
|
||||
: "${BSS_SEC:=0}"
|
||||
: "${BSS_DMA:=0}"
|
||||
|
||||
BANK=32768
|
||||
ITCM_BYTES=$(( TEXT_ITCM + ARM_EXIDX ))
|
||||
ITCM_BANKS=$(( (ITCM_BYTES + BANK - 1) / BANK ))
|
||||
DTCM_TOTAL=$(( (16 - ITCM_BANKS) * BANK ))
|
||||
DATA_BSS=$(( DATA_SEC + BSS_SEC ))
|
||||
FREE_STACK=$(( DTCM_TOTAL - DATA_BSS ))
|
||||
|
||||
# Also report RAM2 (OCRAM) usage for completeness.
|
||||
RAM2_TOTAL=$(( 512 * 1024 ))
|
||||
RAM2_FREE=$(( RAM2_TOTAL - BSS_DMA ))
|
||||
|
||||
echo "=== Teensy 4.1 FlexRAM stack gauge ==="
|
||||
echo " .text.itcm : $(printf '%7d' $TEXT_ITCM) bytes"
|
||||
echo " .ARM.exidx : $(printf '%7d' $ARM_EXIDX) bytes"
|
||||
echo " ITCM total : $(printf '%7d' $ITCM_BYTES) bytes -> $ITCM_BANKS banks ($(( ITCM_BANKS * BANK )) bytes)"
|
||||
echo " .data (DTCM) : $(printf '%7d' $DATA_SEC) bytes"
|
||||
echo " .bss (DTCM) : $(printf '%7d' $BSS_SEC) bytes"
|
||||
echo " DTCM total : $(printf '%7d' $DTCM_TOTAL) bytes ($(( 16 - ITCM_BANKS )) banks)"
|
||||
echo " data + bss : $(printf '%7d' $DATA_BSS) bytes"
|
||||
echo " FREE STACK : $(printf '%7d' $FREE_STACK) bytes (threshold: $MIN_STACK)"
|
||||
echo " ---"
|
||||
echo " .bss.dma RAM2: $(printf '%7d' $BSS_DMA) / $RAM2_TOTAL bytes (free: $RAM2_FREE)"
|
||||
|
||||
if [ "$FREE_STACK" -lt "$MIN_STACK" ]; then
|
||||
echo ""
|
||||
echo " ❌ FAIL: free stack $FREE_STACK < threshold $MIN_STACK"
|
||||
echo " The boot ROM will allocate $ITCM_BANKS ITCM banks, leaving only"
|
||||
echo " $DTCM_TOTAL bytes of DTCM. .data+.bss needs $DATA_BSS bytes."
|
||||
echo " Reduce ITCM (route more code to FLASH) or reduce .data/.bss"
|
||||
echo " (move .rodata to FLASH)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✅ OK: free stack $FREE_STACK >= threshold $MIN_STACK"
|
||||
exit 0
|
||||
+105
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
+16
File diff suppressed because one or more lines are too long
+756
@@ -0,0 +1,756 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkAnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCnkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaWzU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnka.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: fallback;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
+11
@@ -0,0 +1,11 @@
|
||||
.md-header{
|
||||
background-color:#172360 !important;
|
||||
}
|
||||
.md-typeset mark{
|
||||
background-color:blue;
|
||||
color: #fff;
|
||||
}
|
||||
.md-typeset table:not([class]) td{
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1084
File diff suppressed because one or more lines are too long
+5
@@ -0,0 +1,5 @@
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-KRSVT6V');
|
||||
+887
File diff suppressed because one or more lines are too long
+1084
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1594
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
"use strict";(()=>{function c(s,n){parent.postMessage(s,n||"*")}function d(...s){return s.reduce((n,e)=>n.then(()=>new Promise(r=>{let t=document.createElement("script");t.src=e,t.onload=r,document.body.appendChild(t)})),Promise.resolve())}var o=class extends EventTarget{constructor(e){super();this.url=e;this.m=e=>{e.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:e.data})),this.onmessage&&this.onmessage(e))};this.e=(e,r,t,i,m)=>{if(r===`${this.url}`){let a=new ErrorEvent("error",{message:e,filename:r,lineno:t,colno:i,error:m});this.dispatchEvent(a),this.onerror&&this.onerror(a)}};let r=document.createElement("iframe");r.hidden=!0,document.body.appendChild(this.iframe=r),this.w.document.open(),this.w.document.write(`<html><body><script>postMessage=${c};importScripts=${d};addEventListener("error",({error})=>{parent.dispatchEvent(new ErrorEvent("error",{filename:"${e}",error}))})<\/script><script src=${e}?${+Date.now()}><\/script></body></html>`),this.w.document.close(),onmessage=this.m,onerror=this.e,this.r=new Promise((t,i)=>{this.w.onload=t,this.w.onerror=i})}terminate(){this.iframe.remove(),onmessage=onerror=null}postMessage(e){this.r.catch().then(()=>{this.w.dispatchEvent(new MessageEvent("message",structuredClone({data:e})))})}get w(){return this.iframe.contentWindow}};window.IFrameWorker=o;location.protocol==="file:"&&(window.Worker=o);})();
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user