# Heartwood: hardveres Nostr aláíró ESP32-n

**Projekt:** forgesworn/heartwood-esp32
**Forrás:** https://github.com/forgesworn/heartwood-esp32 (SECURITY-MODEL.md, README.md, docs/nip-nonce-commitment.md)
**Weboldal:** https://heartwood.forgesworn.dev/
**Licenc:** MIT · **Nyelv:** Rust (a repó ~90%-a)
**Verzió:** v0.16.0 (stabil, 2026-08-14) · 0.18.0-beta.19 (2026-09-25) — a landing page még v0.15.0-t ír
**Aktivitás:** napi commitok 2026-04 óta; a legutóbbi release-ig 204 PR
**Státusz:** beta, önálló fejlesztői projekt (★1 a repón)

## Mi ez

Nostr **hardveres aláíró** (NIP-46 remote signer), ami egy ESP32 dev boardon fut.
A master seed a chip hardveres entrópiájából születik, egyszer megjelenik az OLED-en,
és **semmilyen kódúton nem hagyja el a device-ot**: nem USB-n, nem relayen, nem
backupban. A NIP-44 visszafejtés, a policy-kiértékelés és maga a **BIP-340 aláírás**
is a chipen történik. A gép, amibe dugod, csak ciphertextet lát.

A modell lényege a **kétfokozatú engedélyezés**: egy előre telepített *policy*
(pontos metódus- és event-kind allowlist) dönti el, mi mehet aláírás nélkül, és egy
**fizikai gomb** minden olyat, amit az allowlist nem fed le. A policy **fail-closed**:
amin kívül esik, azt elutasítja, és **utólag semmilyen gomblenyomás nem eszkalálhatja**.

## Hardver

Négy board, egy Rust firmware, board a fordításkor választva. A szignált update
board-id-hez kötött, ezért a device **elutasítja a más boardra épített firmware-t**.

| Board | Chip | Kijelző | Megjegyzés |
|---|---|---|---|
| Heltec WiFi LoRa 32 V3 | ESP32-S3 | 128×64 OLED | a „munkás” — olcsó, mindenhol kapható |
| Heltec WiFi LoRa 32 V4 | ESP32-S3 | 128×64 OLED | natív USB, **default build target**; L76K GNSS |
| LilyGO/TTGO/TENSTAR T-Display | classic ESP32 | színes TFT | **két gomb** — recovery phrase beírása sokkal kevésbé szenvedés |
| Waveshare ESP32-C6 | RISC-V | 1.47″ színes LCD | legkisebb asztali lábnyom |

A LoRa (SX1262) **soha nincs inicializálva** — nincs használati eset aláíráshoz.
BLE beépítve, de a portable mód még roadmap (nem built).

## Üzemmódok

- **Home HSM — USB-n Raspberry Pi-re dugva (shipped, high-assurance default).**
  A Pi futtatja a `heartwood-bridge`-et: az intézi a hálózatot (Nostr relay-ek,
  NIP-46 transport), az ESP32 az összes kriptográfiát. **A kompromittált Pi nem tud
  master kulcsot kinyerni és nem tud slot-policyt szélesíteni.** Ebben a módban
  minden rádió ki van kapcsolva.
- **WiFi-standalone — on-chip relay kliens (shipped, opt-in).** A chip maga lép be
  WiFi-re és kapcsolódik a relay-ekhez, teljes NIP-46 loop a chipen (`firmware/src/relay.rs`),
  **Pi nélkül**. Ez a kényelmi szint, és **nagyobb támadási felület**: élő TCP/IP stack
  egy kulcsot tartó device-on. Az unbound relay peer nem tud belépni a 30 másodperces
  gomb-loopba — idegen nem tudja lekötni a shelf signert promptokkal. Csak akkor indul,
  ha a device SSID + relay listával van provisionálva; az USB kábel **párhuzamosan
  használható marad**, így egy rossz SSID vagy relay kábelen keresztül helyreállítható.
- **Portable signer — BLE, telefonra (roadmap, nem built).** A home HSM-ből derivált
  **gyerek kulcsot** tart (`purpose="device/mobile"`). Ha elveszik, azt az egy ágat
  égeted el a HSM-en és a következő indexen újat deriválsz — a root és a többi ág
  érintetlen.

## Kulcshierarchia (`nsec-tree`)

```
Master secret (home HSM)
├── persona/social       — publikus Nostr identitás
├── persona/forgesworn   — projekt identitás
├── client/bray          — NIP-46 kliens kulcs
├── device/mobile-0      — portable signer #0 ← gyerek kulcs él itt
├── device/mobile-1      — csere, ha #0 kompromittálódott
└── ...
```

Az `nsec-tree` determinisztikus Nostr al-identitás-derivációt ad: egy master
secretből korlátlan identitás, és minden device saját ágat kap. **Egy gyerek
kompromittálódása sosem fenyegeti a rootot vagy a testvéreket.** Egy device-on
legfeljebb **8 master identity**, három módban (bunker / tree-mnemonic / tree-nsec).

## Threat-modell — mi ellen véd, és mi ellen nem

Ez a rész a projekt legőszintébb dokumentuma (`SECURITY-MODEL.md`, 58 KB).

### Ami ellen VÉD

- **Távoli / hálózati támadó — resisted.** Remote management operátor-hitelesített,
  egyszer használatos mutációs challenge-dzsel, ami **tartósan rotálódik a dispatch
  előtt**; a RAM request-id halmaz a többszörös kézbesítést kezeli élő relay-eken.
  Az `sign_event` policy-gated, authority fizikai jelenlétből vagy explicit
  operátor-policy-ból.
- **Kompromittált operátor kulcs vagy böngésző — bounded, recoverable.** A v2 kliensek
  atomi, szigorú metódus + event-kind plafont kapnak.
- **Elveszett/ellopott unlock telefon — bounded; Sapwood-ból revokálható.**
- **Rosszindulatú firmware — signált USB OTA.** Két dolgot ellenőriz: a kép SHA-256-ját
  (*integritás*) és egy **ed25519 release aláírást** (*autentikusság*). A publikus kulcs
  a firmware-be égetve (`ota-release-pubkey.hex` → `release_key.rs`), az aláírást a CI
  készíti (`release.yml`), és a device **kétszer** ellenőrzi: `OTA_BEGIN`-nél (aláíratlan
  képet visszautasít, mielőtt a tulajdonostól kérdeznének) és `OTA_FINISH`-nél a
  flash-be ténylegesen írt bájtokból újraszámolt digesten. Az aláírt üzenet **board-id-vel
  domain-separated**, így egy board képe nem replayelhető másikra. **Remote OTA nincs
  implementálva** — egy távoli manager nem tudja az `op_mgmt` birtoklását kód-végrehajtássá
  alakítani. OTA ráadásul USB-only és 2 másodperces fizikai nyomást igényel.

### Ami ellen NEM véd (a kritikus rész)

**Fizikai hozzáférés — csak akkor véd, ha a device „sealed at rest”.** A **default
konfigurációban** flash encryption, NVS encryption és secure boot **mind kikapcsolva**
(`sdkconfig.defaults`):

- A **master seed plaintextben** van az NVS-ben — hacsak a PIN vagy a vault-key seal
  nincs bekapcsolva, amely esetben a flash csak ciphertextet ad.
  **Egy „unsealed” device és egy USB kábel elég: `esptool.py read_flash` kihozza a
  32 bájtos seedet.**
- A **WiFi jelszó és a network tranzakció-jelölt plaintextben** van az NVS-ben.
  A NIP-44 a távoli szállítást védi, nem az ESP32-n való tárolást.
- **Secure boot nincs** → tetszőleges firmware flashelhető a ROM bootloaderen,
  **megkerülve a gombos OTA-jóváhagyást teljesen**.
- A **boot PIN csak az alkalmazás frame-loopját** zárja, a ROM bootloadert vagy a nyers
  flash-olvasást nem — a seedet nyugalmi állapotban nem védi.

**Tehát unsealed device esetén a fizikai birtoklás = az összes rajta lévő kulcs teljes
kompromittálódása.** Shelf/server signernél fizikai biztonság mögött ez elfogadható lehet;
egyébként be kell kapcsolni a PIN-t vagy a vault key-t.

**Döntés: az eFuse-alapú hardening kimarad (out of scope).** Az ESP32-S3 *tudná*
kriptográfiailag zárni a rést (`CONFIG_SECURE_BOOT=y`, `CONFIG_FLASH_ENCRYPTION_ENABLED=y`,
`CONFIG_NVS_ENCRYPTION=y`). **Ezt tudatosan elvetették:** az eFuse-ok égetése
**irreverzibilis**, valós brick-kockázatot hordoz, és bonyolítaná a flash/OTA/recovery
workflow-t. A fizikai-access rés így **elfogadott korlát**, operacionálisan mérsékelve
(legyen nálad a device; az elveszett device-t tekintsd kompromittált kulcsnak és
forgasd újra egy új identitás flashelésével).

### Az eFuse-mentes megoldás: PIN-ből derivált seed-titkosítás (P5, BUILT, opt-in)

Az egyetlen hardening-kar, ami **nem nyúl eFuse-hoz**. PIN beállításakor minden master
seed ciphertextként tárolódik: `PBKDF2-HMAC-SHA256(pin, salt)` deriválja a kulcsot,
ChaCha20 + HMAC-SHA256 encrypt-then-MAC titkosítja (`common/src/seed_cipher.rs`), a
plaintext kulcs törlődik.

**A törlés azonban nem felülírás.** Amíg a flash szektor nincs újrahasznosítva, egy nyers
`esptool read_flash` **még mindig kihozhatja a plaintext seedet** a sealing előtti
állapotból. Az élő rekord ciphertext — de a szektor-törmelék nem.

Nincs tárolt PIN-hash: egy gyors hash lehetővé tenné a flash-dump támadónak a PIN olcsó
brute-force-át és a lassú KDF megkerülését, ezért **az AEAD tag az egyetlen PIN-ellenőrzés**,
így minden tipplete megfizeti a PBKDF2 költséget. 5 hibás próbálkozás **törli és
ellenőrzi** a flash-time `config` forrást és a teljes NVS partíciót, hogy a régi
WiFi/operator állapot ne tudja újra-seedelni magát a wipe után.

Van host-oldali **vault key** út is: titkosítás nyugalmi állapotban **felügyelet nélküli
reboottal** (szemben a PIN-úttal, ahol boot-nál kézzel kell feloldani).

## Ismert korlátok (a projekt maga sorolja fel)

Ezek nem találgatások — a `SECURITY-MODEL.md` „Known limits (accepted)” szekciója.

- **Nincs `created_at` frissességi ablak semmilyen inbound úton** (firmware relay oldal
  és `heartwoodd` is figyelmen kívül hagyja, redelivered requestet újra feldolgoz).
  Ez **tudatos**: egy ablak eltörné a skew-elt órájú legitim klienseket, miközben a
  tartós challenge már minden mutációt horgonyoz. Amit egy replayelt, korábban érvényes
  request elérhet: mutációk → semmit (tartós egyszer-használatos challenge);
  `sign_event` → friss aláírás a **azonos, már publikált** eventre (a relay-ek a közös
  id-t dedupálják); encrypt/decrypt → a válasz az eredeti kliens pubkey-ére újrazáródik,
  így a replayelő támadó semmit nem olvas; gomb-gated metódusok → egy elfogott request
  újraéleszthet egy 30 s-os promptot (**availability nuisance, sosem approval**, a fizikai
  gomb korlátozza).
- **Nincs bekötött rate limiting.** Az áprilisi design 60 req/60 s per-kliens számlálója
  **létezik** (`policy.rs`), de a dispatch **nem konzultálja**. A visszaélést a
  policy-gating (ismeretlen kliens = csak gomb), az unbound-peer denial halmaz és a
  korlátos approval gépezet fékezi. **A számláló bekötése jövőbeli hardening.**
- **A PBKDF2 KDF költsége identitásonként skálázódik:** az unlock a PBKDF2-HMAC-SHA256
  költséget (100k round) **minden sealed masterre** megfizeti — körülbelül **26 s három
  identitásra** jelenlegi hardveren, ezért a boot unlock 60 s-ot enged. A per-tipp költség
  a lényeg (minden offline brute-force próba megfizeti), de behatárolja, hány identitást
  tolerál egy PIN-felhasználó. A vault-key út ugyanezt a nyugalmi védelmet nyújtja
  identitásonkénti humán-titok költsége nélkül.
- **A bridge-secret rotáció fizikai:** a USB pairing secret csak akkor állítható/cserélhető,
  ha nincs autentikált session, fizikai nyomás alatt (Sapwood: „replace pairing”).
  **Nincs remote rotáció** — a bridge secret sosem megy át a relay úton, így egy kiszivárgott
  pairing a device-on revokálódik, nem a vezetéken.
- **A backup slot- és bridge-secreteket hordoz (seedet SOHA).** `BACKUP_EXPORT` és
  `BACKUP_IMPORT` is autentikált bridge sessiont és fizikai nyomást igényel. Az import
  **semmit nem állít vissza authority-ként**: minden slot újravalidálódik a management
  policy validátoron, a `signing_approved`/`sign_event` és kind-plafonok **lecsupaszodnak**
  (minden slot újra kiérdemli az aláírást fizikailag vagy operátor-policy-ból), és a
  hordozott bridge secret csak a saját külön nyomása alatt települ. **Az export fájl
  tervezés szerint plaintext secrets dump** — ennek megfelelően kell óvni.
- **A legacy NIP-04 egy decryption oracle gyenge kriptóhoz.** A `nip04_decrypt` metódus
  engedélyezése egy slot policy-ban a device-ot AES-256-CBC (autentikálatlan legacy
  titkosítás) decryption oracle-jává teszi → ahol a kliens támogatja, a NIP-44 metódusokat
  kell előnyben részesíteni.
- **Egy device-operátor; identitásonként delegate-ek.** A device-wide `op_mgmt` pubkey a
  management root. Egy identitás delegálható a saját operátorának
  (`set_identity_operator`, device-operator only, challenge-protected, restarttal
  érvényesül) — de a delegate **egyetlen identitásra** korlátozott: nem adhat hozzá
  identitást, nem delegálhat tovább, nem olvas device-wide állapotot (network config,
  audit ring, relay topológia, storage inventory), és **nem tudja felsorolni sem a
  tulajdonos többi identitását**. Két reziduális: a Sapwood telefon-handoff **a
  device-operátor credentialt másolja** ahelyett, hogy függetlenül revokálható delegate-et
  regisztrálna; és a böngészők egy operátort újrahasználhatnak több signer között, növelve
  a kompromittálódás blast radiusát. Magát a device-operátort cserélni trusted-USB, fizikai
  hold marad.
- **Boundolt duplicate history; nincs freshness window:** a RAM-only request-id halmaz
  korlátos (`SEEN_MAX = 64`) és reboottal nullázódik. A polled read id-k NVS-en kívül
  tartása **napi ~43 200 írást spórol** egy 4 másodperces poll intervallummal nyitott
  managernél.

## A megoldatlan kriptográfiai rés: NIP-NONCE-COMMITMENT

Ez a legértékesebb technikai anyag a projektben, és **a projekt maga nyitotta meg**.

**A probléma.** Egy NIP-46 signer azért kapja meg az nsecet, hogy semmi másnak ne kelljen
megkapnia. Egy csatorna marad nyitva: **maga az aláírás**. A BIP-340 engedi, hogy a signer
32 bájt auxiliáris randomit adjon a nonce-derivációjához, és **a protokoll semmi módon nem
korlátozza ezt a választást**. Rosszindulatú firmware az `aux_rand`-ot csiszolhatja, amíg
az így kapott aláírás néhány választott bitet kódol, és **néhány tucat tökéletesen érvényes
aláíráson keresztül egy teljes seedet szivárogtat ki**. A felhasználó normál eseményeket lát,
normálisan aláírva. Semmilyen log, kijelző vagy policy engine nem vesz észre semmit.

**Nem hipotetikus.** A hardware wallet világ már végigment rajta: a Blockstream Jade
**Anti-Exfil** protokollja azért létezik, mert a nonce-on keresztül szivárogtató
kompromittált aláíró device-ot **reális fenyegetésnek** ítélték Bitcoinon. A nyílt forrás és
a reprodukálható buildek szűkítik a rosszindulatú firmware ablakát, de nem zárják be:
egy verifier csak azt a buildet tudja ellenőrizni, amit verifikált, nem azt, ami leszállt.

**A megoldás (sign-to-contract).** A signer **elkötelezi magát a nonce-pontja mellett**,
mielőtt megtudná a kliens által hozzáadott randomitást; a végleges nonce a committed pont,
módosítva mindkettő hash-ével. **A signer nem tudja csiszolni azt, amit nem tud
megjósolni.**

**A protokoll:** két további NIP-46 metódus, `sign_event_commit` és `sign_event_reveal`,
amik rendes NIP-46 requestként utaznak kind 24133 envelopban. Első körben a kliens 32 friss
random bájtot (`rho`) húz, és **a hash-ét küldi el, a `rho`-t titokban tartva**; a signer
erre commitálja a nonce-pontját. A második körben a kliens felfedi a `rho`-t, és a végleges
nonce mindkettő függvénye.

**Státusz:** publikálva draft community NIP-ként, kind 30817, identifier
`nip-nonce-commitment` (`docs/nip-nonce-commitment.md`). **A firmware-implementáció jövőbeli
munka** — ma a védelem a szignált, CI-buildelt release chain plusz a szándékos rollback
képesség.

## Ökoszisztéma

A projekt nem egy fájl, hanem egy kis fa („The whole tree”):

- **Heartwood** — a mag: kulcsok a chipen, fizikai gomb mögött.
- **Sapwood** (`forgesworn/sapwood`, TypeScript) — a **management console**: flash,
  provision, policy-k beállítása, approve, backup. Két transport: **USB bootstrap/recovery
  Web Serial API-val**, és **távoli management Nostr relay-eken** át a Heartwood kimenő
  WiFi kapcsolatára. **A device nem nyit bejövő portot**, nem kell cloud fiók vagy
  management szerver. A frame protokoll a `heartwood-common/src/frame.rs` TypeScript portja,
  **19 teszttel** igazolva a bájt-szintű kompatibilitást a Rust implementációval.
  **Saját státusza: „Untested alpha”** — a teljes write-down/wipe/hardware-restore
  ceremónia **még nem futott le**, ezért csak teszt kulcsokkal, független backup mellett.
- **`heartwoodd`** — a daemon (a `heartwood-bridge` oldalán).
- **`nsec-tree`** — determinisztikus Nostr sub-identity deriváció.
- **`bray`** — trust-aware Nostr MCP szerver AI ágensekhez (a `client/bray` kulcs erre utal).

A `forgesworn` org ~30 repót tart, szinte mindet ugyanaznap pusholva, jellemzően 0-3
csillaggal: **szélességében építő egyéni fejlesztő**, nem közösségi projekt. A Heartwood
viszont április óta stabilan fókuszált.

## Miért érdekes

A Nostr kulcsnak **nincs jelszó-resetje**, és a legtöbb mégis a legtámadottabb szoftverben
él: egy böngésző-profilban, egy telefonos appban, egy szerver home könyvtárában. Egy
phishing oldal, egy rosszindulatú update, egy elrontott backup — és az identitás véglegesen
másé.

A Heartwood ezt a kulcsot egy olyan device-ra teszi, aminek **az egyetlen feladata a
megtartása**. Amit ténylegesen kizár: a **távoli és a szoftveres** támadót — vagyis pont azt,
ami a valóságban elviszi az nseceket. Amit **nem** zár ki a default konfigurációban: a
fizikai támadót egy USB kábellel. Ez utóbbihoz be kell kapcsolni a PIN-t vagy a vault
key-t — és akkor is egy ESP32 szintjén véd, nem eFuse-szinten.

A projekt értékét nagyban növeli, hogy **ezt maga mondja ki**, ugyanolyan részletességgel,
mint az erősségeit.

## Kapcsolódó cikkek

- `raw/articles/` — Nostr ökoszisztéma és protokoll anyagok
- `topics/nostr-ecosystem.md` — NIP-46 signer réteg
- `topics/bitcoin-wallet-security.md` — hardver wallet-ok, Coldcard entrópia-eset, Trezor Safe 7
- `raw/references/clink-lightning-nostr.md` — Nostr-native Lightning specifikáció (analóg minta)

## Források

- https://github.com/forgesworn/heartwood-esp32 — README.md, SECURITY-MODEL.md (58 KB), docs/nip-nonce-commitment.md
- https://heartwood.forgesworn.dev/ — projekt weboldal
- https://github.com/forgesworn/sapwood — management console (TypeScript, „untested alpha”)
- https://github.com/forgesworn/nsec-tree — sub-identity deriváció
- Feldolgozva: 2026-09-26
