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_eventpolicy-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) ésOTA_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 azop_mgmtbirtoklá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_flashkihozza 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_atfrissességi ablak semmilyen inbound úton (firmware relay oldal ésheartwooddis 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ésBACKUP_IMPORTis 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, asigning_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_decryptmetó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_mgmtpubkey 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 aheartwood-common/src/frame.rsTypeScript 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 (aheartwood-bridgeoldalán).nsec-tree— determinisztikus Nostr sub-identity deriváció.bray— trust-aware Nostr MCP szerver AI ágensekhez (aclient/braykulcs 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 anyagoktopics/nostr-ecosystem.md— NIP-46 signer rétegtopics/bitcoin-wallet-security.md— hardver wallet-ok, Coldcard entrópia-eset, Trezor Safe 7raw/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