v0.0.47 - Added post-quantum cryptography (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) and standard ECC (ed25519, x25519) support with algorithm-based API

This commit is contained in:
Laan Tungir
2026-07-16 15:14:57 -04:00
parent 6fd7b8ce1f
commit c5f1a70658
66 changed files with 19091 additions and 257 deletions

206
README.md
View File

@@ -169,6 +169,145 @@ A single memorized mnemonic can deterministically recover multiple key domains t
Examples include Nostr roles, Bitcoin branches, and future application-specific paths. See [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md) for the maintained use-case catalog and caveats.
## 4b. Crypto palette
`n_signer` supports six cryptographic algorithms, all derived deterministically from the same BIP-39 mnemonic. Each algorithm is bound to a specific `(purpose, curve)` pair and a distinct BIP-44 derivation path.
### 4b.1 Algorithms
| Algorithm | Curve label | Purpose | FIPS standard | Key sizes (priv / pub) | Derivation path |
|---|---|---|---|---|---|
| secp256k1 | `secp256k1` | `nostr` | — | 32 / 32 bytes | `m/44'/1237'/<n>'/0/0` (NIP-06) |
| ed25519 | `ed25519` | `ssh` | — | 32 / 32 bytes | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) |
| x25519 | `x25519` | `age` | — | 32 / 32 bytes | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) |
| ML-DSA-65 | `ml-dsa-65` | `pq-sig` | FIPS 204 | 4032 / 1952 bytes | `m/44'/102003'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
| SLH-DSA-128s | `slh-dsa-128s` | `pq-sig` | FIPS 205 | 64 / 32 bytes | `m/44'/102004'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
| ML-KEM-768 | `ml-kem-768` | `pq-kem` | FIPS 203 | 2400 / 1184 bytes | `m/44'/102005'/<n>'/0'/0'` → seed → DRBG → PQClean keygen |
All six algorithms are always compiled in on every target (host x86_64 static binary and ESP32 firmware). The PQ implementations are vendored from [PQClean](https://github.com/PQClean/PQClean) (public domain / CC0).
### 4b.2 Key derivation
All keys derive deterministically from the BIP-39 mnemonic:
- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar.
- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key.
- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. This produces deterministic PQ key pairs from the mnemonic — same mnemonic, same role, same key pair every time. See [`documents/SECURITY.md`](documents/SECURITY.md) §17 for the security argument.
The new algorithms use BIP-44 coin types `102001``102005` (unregistered in SLIP-44, chosen to avoid collisions with real cryptocurrencies). All non-secp256k1 paths are fully hardened per SLIP-0010.
### 4b.3 Post-quantum context
The three post-quantum algorithms address the **harvest-now-decrypt-later** threat: an adversary recording encrypted traffic today to decrypt it once a quantum computer becomes available. ML-KEM-768 protects key agreement against this threat. ML-DSA-65 and SLH-DSA-128s protect signatures against future quantum forgery.
All three are FIPS-standardized (FIPS 203, 204, 205) and are provided as **additional options**, not replacements for secp256k1. Nostr continues to use secp256k1 exclusively. PQ algorithms are opt-in per role.
Note: OpenSSH does not yet support PQ signing keys. The `pq-sig` purpose is forward-looking — the primitives are ready for when the ecosystem adopts them.
### 4b.4 Structured `get_public_key` response
For secp256k1, `get_public_key` returns the plain 64-hex-char public key string (backward compatible). An optional `{"format": "structured"}` in the options object requests a structured JSON response.
For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s, ML-KEM-768), `get_public_key` always returns a structured JSON object (serialized as a string):
```json
{"algorithm": "ml-dsa-65", "public_key": "<hex>", "key_id": "<16 hex chars>"}
```
The `key_id` is a short display identifier (first 16 hex chars of the public key). PQ public keys are large (ML-DSA-65: 3904 hex chars; ML-KEM-768: 2368 hex chars).
## 4c. Algorithm-based API
In addition to the role-based API, n_signer supports an **algorithm-based API** where the caller specifies the algorithm and derivation index directly, without needing to know role names. This is the preferred API for new clients.
### 4c.1 New verbs
| Verb | Description | Algorithm parameter | Key parameter |
|---|---|---|---|
| `sign` | Sign arbitrary bytes | `algorithm` | `index` |
| `verify` | Verify a signature | `algorithm` | `index` |
| `encapsulate` | KEM encapsulation | `algorithm` | `public_key` (peer's) |
| `decapsulate` | KEM decapsulation | `algorithm` | `index` |
| `derive_shared_secret` | ECDH key agreement (x25519) | `algorithm` | `index` + `peer_public_key` |
| `get_public_key` | Get public key (algorithm-based) | `algorithm` | `index` |
### 4c.2 Algorithm names
| String | Algorithm | Key type |
|---|---|---|
| `secp256k1` | secp256k1 (Schnorr/ECDSA) | Signature |
| `ed25519` | ed25519 | Signature |
| `ml-dsa-65` | ML-DSA-65 (FIPS 204) | Signature |
| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205) | Signature |
| `x25519` | X25519 (ECDH) | Key agreement |
| `ml-kem-768` | ML-KEM-768 (FIPS 203) | KEM |
### 4c.3 Request examples
**Sign with ed25519:**
```json
{"id":"1","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}
```
Response: `{"id":"1","result":{"signature":"<hex>","algorithm":"ed25519","key_id":"<16hex>"}}`
**Sign with secp256k1 (ECDSA scheme):**
```json
{"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"secp256k1","index":0,"scheme":"ecdsa"}]}
```
The `scheme` parameter is optional for secp256k1: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
**Get public key (algorithm-based):**
```json
{"id":"3","method":"get_public_key","params":[{"algorithm":"ml-dsa-65","index":0}]}
```
Response: `{"id":"3","result":{"algorithm":"ml-dsa-65","public_key":"<hex>","key_id":"<16hex>"}}`
**KEM encapsulate:**
```json
{"id":"4","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]}
```
**KEM decapsulate:**
```json
{"id":"5","method":"decapsulate","params":["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]}
```
**ECDH shared secret (x25519):**
```json
{"id":"6","method":"derive_shared_secret","params":["<peer_pubkey_hex>",{"algorithm":"x25519","index":0}]}
```
### 4c.4 Old verb aliases
Old verb names map to the new algorithm-based verbs when used with the `algorithm` parameter:
| Old verb | New verb | Default algorithm |
|---|---|---|
| `sign_data` | `sign` | (from `algorithm` parameter) |
| `ssh_sign` | `sign` | `ed25519` |
| `verify_signature` | `verify` | (from `algorithm` parameter) |
| `kem_encapsulate` | `encapsulate` | `ml-kem-768` |
| `kem_decapsulate` | `decapsulate` | `ml-kem-768` |
Without the `algorithm` parameter, old verbs fall through to the role-based path (backward compatible).
### 4c.5 Algorithm-based preapprove
```
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify
nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate
```
### 4c.6 Enforcement matrix
| Verb | Valid algorithms |
|---|---|
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
| `encapsulate` / `decapsulate` | ml-kem-768 |
| `derive_shared_secret` | x25519 |
| `get_public_key` | all algorithms |
| `sign_event` / `nip44_*` / `nip04_*` / `mine_event` | secp256k1 (Nostr protocol) |
## 5. Wire contract (JSON-RPC)
Request shape is JSON-RPC with NIP-46-style methods and optional trailing selector options.
@@ -183,12 +322,37 @@ Request shape is JSON-RPC with NIP-46-style methods and optional trailing select
Implemented signer verbs in this build:
**Nostr verbs** (require `purpose="nostr"`, `curve="secp256k1"`):
- `get_public_key`
- `sign_event`
- `nip04_encrypt` / `nip04_decrypt`
- `nip44_encrypt` / `nip44_decrypt`
- `mine_event` — add NIP-13 proof-of-work and sign (see below)
**Multi-algorithm verbs**:
| Verb | Required purpose | Required curve | Description |
|---|---|---|---|
| `get_public_key` | any | any | Returns the role's public key. secp256k1 returns plain hex; other algorithms return structured JSON (see [§4b.4](#4b4-structured-get_public_key-response)). |
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | Sign arbitrary bytes (not a Nostr event). Returns `{"signature":"<hex>","algorithm":"<alg>"}`. |
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | Verify a signature. Returns `{"valid":true/false}`. |
| `ssh_sign` | `ssh` | `ed25519` | Sign an SSH authentication challenge in ed25519 format. Returns `{"signature":"<hex>","algorithm":"ed25519"}`. |
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` | Encapsulate against a peer's ML-KEM-768 public key. Returns `{"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}`. |
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` | Decapsulate a ciphertext using the role's ML-KEM-768 private key. Returns `{"shared_secret":"<hex>","algorithm":"ml-kem-768"}`. |
Example `sign_data` request:
```jsonc
{ "id": "6", "method": "sign_data", "params": ["<message_hex>", { "role": "pq-ml-dsa" }] }
```
Example `kem_encapsulate` request:
```jsonc
{ "id": "7", "method": "kem_encapsulate", "params": ["<peer_pubkey_hex>", { "role": "pq-kem-main" }] }
```
### `mine_event` — NIP-13 Proof-of-Work
Mines proof-of-work (adds a `nonce` tag per NIP-13) and signs the event in one step. The mining runs in a detached thread so the server stays responsive.
@@ -249,13 +413,52 @@ Representative error codes:
Selector resolution chooses *which* role. Enforcement decides *whether the requested method is valid* for that role.
### 6.1 Purpose values
| Purpose | Description |
|---|---|
| `nostr` | Nostr identities (secp256k1, NIP-06) |
| `bitcoin` | Bitcoin key trees (secp256k1, BIP-44) |
| `ssh` | SSH signing keys (ed25519) |
| `age` | age-style encryption identities (x25519) |
| `fips` | FIPS mesh/service identities |
| `pq-sig` | Post-quantum signatures (ML-DSA-65, SLH-DSA-128s) |
| `pq-kem` | Post-quantum key encapsulation (ML-KEM-768) |
### 6.2 Curve values
| Curve | Algorithms |
|---|---|
| `secp256k1` | ECDSA/Schnorr for Nostr, Bitcoin |
| `ed25519` | Ed25519 for SSH signatures |
| `x25519` | X25519 for key agreement (age) |
| `ml-dsa-65` | ML-DSA-65 (FIPS 204, lattice-based PQ signatures) |
| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205, hash-based PQ signatures) |
| `ml-kem-768` | ML-KEM-768 (FIPS 203, lattice-based PQ KEM) |
### 6.3 Enforcement matrix
| Verb | Required purpose | Required curve |
|---|---|---|
| `sign_event` | `nostr` | `secp256k1` |
| `mine_event` | `nostr` | `secp256k1` |
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` |
| `get_public_key` | any | any (must match role's declared curve) |
| `sign_data` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` |
| `ssh_sign` | `ssh` | `ed25519` |
| `kem_encapsulate` | `pq-kem` | `ml-kem-768` |
| `kem_decapsulate` | `pq-kem` | `ml-kem-768` |
Example:
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
- `mine_event` requires `purpose="nostr"` and `curve="secp256k1"` (same as `sign_event`).
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
- If caller selects a `pq-sig`/`ml-dsa-65` key for `sign_event`, request fails with `purpose_mismatch`.
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
This prevents cross-protocol misuse inside one mnemonic-rooted signer process. Fail-closed: any unlisted `(verb, purpose, curve)` combination is rejected.
## 7. Transport
@@ -509,6 +712,7 @@ These items are designed and worth doing, but are not in the current implementat
- [`examples/get_pubkey_tcp.c`](examples/get_pubkey_tcp.c): C caller example using TCP transport with auth envelope
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
- [`plans/post_quantum_crypto.md`](plans/post_quantum_crypto.md): post-quantum and multi-algorithm crypto expansion plan (all 8 phases)
- [`firmware/feather_s3_tft`](firmware/feather_s3_tft): Feather ESP32-S3 Reverse TFT firmware (TinyUSB composite CDC + WebUSB signer PoC)
- [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md): firmware Phase 7b plan and outcome (TinyUSB composite USB transport)
- [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md): browser extension exposing NIP-07 over `nsigner`