Lees dit voor je ergens op klikt
Een browsertabblad is geen hardware wallet. Het heeft geen secure element. Het heeft geen air gap. Het draait op een besturingssysteem voor algemeen gebruik, binnen een JavaScript-omgeving, naast elk ander tabblad en elke browserextensie die je geïnstalleerd hebt, en het wordt bediend door mij, een uitbater die je anders nergens voor zou hoeven vertrouwen.
Typ nooit een seed phrase waar je op steunt in deze simulator. Ook niet om "even iets te checken". Geen enkele keer. Alles wat de simulator bijhoudt, staat in gewoon geheugen, is zichtbaar in de ontwikkelaarstools, en is één schermafbeelding, één wisselbestand of één kwaadaardige extensie verwijderd van iemand anders.
Wil je een seed om mee te spelen, gebruik dan een publieke testseed, een wegwerp-seed phrase die geen bitcoin bevat en dat ook nooit zal doen. Er bestaan gepubliceerde lijsten daarvan: hun sleutels zijn al publiek, al leeg, en zitten al in elke bitcoin-testsuite ter wereld. Dat is net waar ze voor dienen. Typ zo eentje in.
Voerde je al een echte seed in, beschouw die dan als gecompromitteerd, maak een nieuwe aan op een toestel dat je vertrouwt, en verplaats je middelen. Ga niet afwegen hoe groot de kans is.
Dat gezegd zijnde, hier is het ding zelf.
Wat het is
Open bitsaga.be/seedsigner-simulator en je krijgt een SeedSigner. Geen foto ervan, geen schermopname, geen namaak van de menu's in HTML. De Python die op het toestel draait, draait in het tabblad, ongewijzigd, en de eigen Controller.start() van de wallet stuurt alles aan. De menuboom, de omgang met seeds, de logica van passphrases, het ontleden van PSBT's, de QR-encoders: allemaal code van upstream, die doet wat ze op echte hardware doet.
De bediening: pijltjestoetsen bewegen, Enter selecteert, en 1 2 3 zijn de drie zijknoppen. Je kunt ook gewoon op de knoppen van de toesteltekening klikken, wat op hetzelfde neerkomt. De eerste keer laden haalt ongeveer 30 MB runtime en wallet binnen; daarna werkt de pagina offline. Ga je naar een scanscherm, dan vraagt hij je webcam; richt die op een SeedQR en de wallet laadt de seed, want dat is echt het camerapad en geen kortere weg.
Het geheel is open source en staat op GitHub bij bitsagarob/seedsigner-sim. Alles hieronder valt na te gaan aan de hand van die repository, en het meeste ervan valt nergens anders na te gaan, en dat is net de bedoeling.
De hardware die hij nabootst
De SeedSigner die de simulator nabootst, is de build met smartcardondersteuning: de variant met het grotere scherm, een richtingspad in plaats van een joystick, en een kaartlezer waar een Satochip SeedKeeper-kaart in gaat. Hij ligt nu in de Bitsaga-shop, en dan specifiek de in 3D geprinte behuizing, in het zwart of oranje. De versie in CNC-gefreesd aluminium is nog niet beschikbaar, maar komt binnenkort.
En dat is grotendeels de reden om überhaupt een simulator online te zetten: je kunt ontdekken of je het toestel graag hebt voor iemand je om geld vraagt. Doorloop de stappen die je echt zou doen (een seed laden, een passphrase toevoegen, een xpub exporteren, een PSBT ondertekenen, een seed back-uppen als SeedQR) en beslis van daaruit.
Hoe het werkt
Dit deel is het lezen waard, ook als je nooit iets koopt, want zowat elke ontwerpkeuze werd afgedwongen door een beperking in plaats van gekozen, en die beperkingen zijn interessanter dan de code.
De firmware wordt niet aangepast. Ze wordt omsingeld.
De Python van de wallet draait onder Pyodide (CPython 3.12 gecompileerd naar WebAssembly) binnen een Web Worker. Wat er geladen wordt, is een bestand met de naam wallet-smartcard.zip, of wallet-stock.zip als je de firmware op de pagina omschakelt: het upstream-pakket seedsigner , de pure Python-bibliotheken die het importeert (embit, pysatochip, qrcode, mnemonic, urtypes, ecdsa en de rest), en één pakket dat dit project zelf schreef. De zip wordt uitgepakt in het geheugenbestandssysteem van Pyodide op /wallet, dat de werkmap wordt, en daarna wordt het eigen startpunt van upstream aangeroepen.
Elk stuk nagebootste hardware wordt van buitenafingelapt, op de laagst beschikbare naad, nadat de wallet uitgepakt is en voor ze start. Er wordt niets binnenin seedsigner/ gewijzigd. Dat is geen kwestie van stijl: het is de enige versie van de bewering "dit is de echte firmware" die iemand kan controleren, want een aangepaste boom en een onaangepaste boom hashen verschillend, en een lezer kan allebei berekenen. De shim-modules zitten bewust niet eens in de zip: ze worden apart opgehaald en er bij het opstarten naast geschreven, zodat de zip precies blijft wat het buildscript maakte en de naden zichtbaar buiten die zip liggen.
De beperking waar al de rest uit volgt
De hoofdlus van SeedSigner blokkeert de processor in afwachting van een knopdruk. Op een Raspberry Pi is dat correct gedrag: er is niets anders voor de processor te doen. In een browser betekent het dat de thread die die lus draait nooit terugkeert naar zijn event loop, en dat heeft één gevolg, en dat gevolg is asymmetrisch.
Naar buiten uit de worker werkt nog altijd. Python roept een JavaScript-callback aan, die callback roept postMessageaan, en de pagina ontvangt het. Schermbeelden en logregels reizen zo probleemloos.
Naar binnen in de worker is onmogelijk. A postMessage dat naar een geblokkeerde worker gestuurd wordt, belandt in een wachtrij die nooit leeggemaakt zal worden, want leegmaken vereist dat de blokkerende lus terugkeert, en dat doet ze pas als de gebruiker op de knop drukt die je net probeert af te leveren. Het bericht en datgene waarop het wacht, zitten op elkaar vast in een deadlock.
Daarom steekt elke invoer over via een SharedArrayBuffer in de plaats: geheugen dat beide threads zien, dat de worker synchroon leest en waarop hij wacht met Atomics.wait tot de pagina hem aanstoot met Atomics.notify. Toetsen, camerabeelden en de kaartlade krijgen elk hun eigen buffer.
Daarom heeft de pagina cross-origin isolatie. SharedArrayBuffer nodig: die is enkel construeerbaar op een document dat bediend wordt met Cross-Origin-Opener-Policy: same-origin en Cross-Origin-Embedder-Policy: require-corp; zonder die twee headers bestaat de constructor niet en is er helemaal geen weg naar binnen in de worker. De pagina controleert crossOriginIsolated nog voor ze de worker start, en zegt dat met zoveel woorden in plaats van stilletjes te falen, want dit is hét ding dat elke eerste poging tot zelf hosten doet stranden. Het is ook waarom de wallet in een worker draait en niet op de eigen thread van de pagina: dat blokkeren is prima, zolang het gebeurt op een plek waar de interface niet zit.
Het scherm wordt een canvas
SeedSigner tekent via zijn eigen Renderer naar een driver voor het ingestelde paneel. De simulator voegt nog een driver toe: dezelfde BaseDisplayDriver basisklasse, hetzelfde show_image contract, alleen geeft deze de ruwe RGB-bytes van het beeld door aan een callback waar een echte driver pixels over SPI uitklokt. Hem installeren komt neer op één fabrieksmethode vervangen, zodat elk ingesteld schermtype de browserdriver oplevert.
Niets boven die lijn merkt het verschil. De Renderer, elke Screen, elke View, de lettertypes, de opmaakcode: allemaal ongewijzigd. Het beeld gaat van worker naar pagina als een overdraagbare byte-array, de pagina zet RGB om naar RGBA en doet één putImageData, en het canvas zit in de uitsparing van een SVG-tekening van het toestel, in percentages gepositioneerd zodat de twee bij elke vensterbreedte uitgelijnd blijven.
De knoppen worden je toetsenbord
Acht bytes gedeeld geheugen, bekeken als twee slots van 32 bit: het ene zegt "er wacht een toets", het andere welke. De pagina schrijft de code, zet de vlag, en verwittigt. De worker wacht op Atomics.wait, wordt wakker, leest de code en wist de vlag. Aan de Python-kant wordt dat HardwareButtons.wait_for, en dat is wat de hele wallet aanroept om een knop uit te lezen.
Er is een tweede, niet-blokkerend pad, en de reden daarvoor illustreert mooi hoe dun de nabootsing moet zijn. Het scanscherm kan niet op een knop blokkeren (het moet tegelijk camerabeelden binnenhalen), dus het pollt in de plaats. Maar één doorloop van die pollus vraagt achtereenvolgens naar meerdere toetsen, dus een druk moet lang genoeg opeisbaar blijven zodat elke controle in die doorloop hem ziet, en toch niet blijven hangen, anders staat een toets die niemand wil vooraan in de rij die erachter te verbergen. Het antwoord is een korte wachtlijst waarin elke druk tot vier keer aangeboden wordt en daarna vervalt. Niet elegant; wel correct.
De camera wordt je webcam, en het decoderen van QR verhuist naar JavaScript
Er worden hier twee dingen nagebootst, niet één, en alleen het eerste is voor de hand liggend. De videostroom vervangen is makkelijk te verantwoorden: een browser heeft getUserMedia en geen picamera. Het decoderen vervangen is het interessante geval.
SeedSigner leest QR-codes met pyzbar, een binding naar de C-bibliotheek zbar. Er bestaat geen zbar voor WebAssembly, en er eentje naartoe porteren zou sowieso het verkeerde antwoord zijn, want de browser heeft zelf al een QR-decoder aan boord. De nabootsing zit dus precies daar waar SeedSigner naar hardware grijpt, en de inhoud wordt aan Python teruggegeven als bytes, in de vorm waarin pyzbar ze teruggegeven zou hebben. Alles boven die lijn (het scanscherm, DecodeQRmet zijn ontleding van SeedQR, CompactSeedQR, PSBT-fragmenten en UR-payloads, en elke weergave die ze gebruikt) draait onaangeroerd.
Technisch: de pagina tekent elk videobeeld in een opnamecanvas van 640×480, waar de QR nog scherp is, en in een voorbeeldcanvas van 240×240, want elke byte van dat voorbeeld wordt naar een Python-beeldobject gekopieerd. Heeft de browser BarcodeDetector, dan krijgt native code de goedkope vraag voorgeschoteld (zit er in dit beeld überhaupt een QR?) en bij zowat elk beeld is het antwoord nee, sneller dan JavaScript het had kunnen zeggen. Is het antwoord ja, dan decodeert jsQR hetzelfde beeld en geeft het de codewoorden zelf terug. Die bytes gaan in de gedeelde buffer, en in de worker negeert de aanroep die vroeger pyzbar was het beeld dat hij kreeg, en geeft hij terug wat de pagina het laatst publiceerde.
Twee volgorderegels houden dat samen. De teller van de beeldvolgorde wordt als laatsteopgehoogd, na de pixels, want die teller vertelt de worker dat de bytes het lezen waard zijn; een worker die middenin een schrijfbeurt leest, krijgt een gescheurd voorbeeldbeeld en niets ergers, want de decodering kijkt nooit naar die bytes. En het payloadslot is een brievenbus voor één stuk: de pagina decodeert geen nieuwe QR zolang er nog één onopgehaald is, en pas wanneer de worker hem wist, gaat de volgende open. Die tweede regel is ook wat verhindert dat een QR die stil voor de camera gehouden wordt de decoder overspoelt met kopieën van zichzelf.
Waarom de eigen decoder van de browser nooit mag zeggen wat er in een QR staat
Dit is de scherpste rand van het hele project en verdient een eigen kopje.
BarcodeDetector legt enkel ooit een rawValue, a stringbloot. Een CompactSeedQR is ruwe entropie: bytes die het niet overleven om als tekens gedecodeerd en opnieuw gecodeerd te worden. De native detector wordt dus als poort gebruikt en niets meer. jsQR, dat codewoorden teruggeeft, is het enige dat een payload mag opleveren, en er is bewust geen terugval naar rawValue wanneer jsQR niets vindt.
De reden is het waard om onomwonden te zeggen. Een verkeerd gelezen string kan nog altijd een plausibele lengtehebben, en 16, 20, 24, 28 of 32 bytes is alles wat de wallet nodig heeft om hem als CompactSeedQR te aanvaarden. Hij zou dan een seed laden, tonen en vrolijk aanbieden om er een back-up van te maken, terwijl die seed nooit voor de camera gehangen heeft. Een scan die mislukt en die je opnieuw moet doen, is vervelend; een verkeerde seed die als de juiste gepresenteerd wordt, valt niet meer recht te zetten. Dat is niet hypothetisch: de regressietest die dit nu afdekt, kwam vanuit pure rommel tot een geldig ogende vingerafdruk voor die terugval geschrapt werd. De test richt een nagebootste camera op een blanco beeld en faalt als de wallet ook maar enige seed meldt.
De smartcard, die browsers helemaal niet hebben
De smartcard-fork van SeedSigner praat met een fysieke kaart via pyscard. Browsers hebben geen enkele smartcard-API, dus de enige eerlijke plek om er een na te bootsen is het transport, en de simulator doet precies dat door een pakket mee te leveren dat is smartcard, de modulenaam die pyscard inneemt, en dat antwoordt met kaarten die in Python geïmplementeerd zijn. Alles daarboven draait onveranderd: heel pysatochip, en alle kaartcode van SeedSigner. De stappen worden echt doorlopen in plaats van nagebootst.
De gesimuleerde Satochip antwoordt op APDU-niveau: SELECT geeft succes terug voor de Satochip-toepassing en "bestand niet gevonden" voor al de rest, en zo komt de kaartdetectie van de wallet überhaupt bij Satochip uit; GET DATA geeft de drie identiteitsblokken terug die pysatochip tot een kaart-UID hasht; GET STATUS geeft het statusblok terug met versies, resterende pincodepogingen en instellingsvlaggen; SETUP neemt een pincode aan en maakt van een blanco kaart een geïnitialiseerde; VERIFY PIN controleert die, verbruikt een poging wanneer hij fout is, en meldt het resterende aantal in het statuswoord. Al de rest komt terug als "niet ondersteund".
Er zijn drie kaarten, want een gebruiker moet ze uit elkaar kunnen houden: zet een pincode op de eerste, bevestig dat de tweede nog blanco is, kom terug en vind de eerste precies zoals je hem achterliet. Ze verschillen in slechts één identiteitsveld, en dat volstaat, want net dat wordt gehasht tot de UID waarmee de wallet kaarten onderscheidt. Welke kaart in de lezer zit, is de zaak van de gebruiker, en die zit op de pagina, dus de lade is de derde gedeelde buffer, en de worker wacht er in stukjes op terwijl de wallet op een kaart wacht.
De rest van de Raspberry Pi die er niet is
Een handvol kleinere ontbrekende zaken veroorzaakt elk een harde fout zonder ingreep, en samen vormen ze een mooie inventaris van wat een Linux-machine stilzwijgend levert:
- Geen threads. Pyodide heeft er geen. De animatiethreads van SeedSigner draaien eindeloos rond op een vlag, dus er eentje synchroon draaien zou nooit terugkeren, en die worden geschrapt; elke andere thread in de codebasis is een eenmalige hulp waarvan de aanroeper op het resultaat wacht, dus die draaien inline. Eén is een uitzondering met naam: de controller blokkeert in afwachting van de thread die de opslag opzet, en zonder die thread blijft de wallet na het startscherm eeuwig hangen.
- Geen
hashlib.pbkdf2_hmac. . Die zit in de OpenSSL-binding, die deze build niet heeft, en ze ligt recht op het pad van een mnemonic naar seed-bytes. Zonder vervanging mislukt elke seed laden. De implementatie van pycryptodome wordt geleend in plaats van de afleiding zelf in elkaar te knutselen. - Geen subprocessen. Verschillende hulpfuncties roepen een sneller native hulpmiddel aan en vallen terug op pure Python wanneer die binary ontbreekt. Emscripten werpt het verkeerde uitzonderingstype op om die terugval te laten aanslaan, dus
subprocessis aangepast om de binary gewoon als afwezig te melden, wat hier klopt en wat upstream sowieso al afhandelt. - Geen OpenCV, geen numpy. Onschadelijk, want het decoderen gebeurt in de browser.
- De ene naam voor de andere.
pycryptodomexenpycryptodomezijn dezelfde bibliotheek onder twee importnamen, dus een padhaak legt de ene op de andere.
Er is een vijfde shim die geen hardwarenaad is, maar wel hetzelfde probleem op een andere plek. De schermen die een QR tonen (geëxporteerde xpubs, ondertekende PSBT's, SeedQR-back-ups, adressen) doen al hun tekenwerk binnen een thread. Zonder threads komen ze er allemaal blanco uit: de stap lijkt te werken en levert een leeg scherm op. In plaats van iets daarvan te herschrijven, draait de shim de eigen luslichaam van upstream precies één doorloop per keer. De laatste opdracht daarin is een pauze die elk beeld een zesde van een seconde vasthoudt, dus één doorloop is precies één animatiebeeld op het bedoelde tempo, en geanimeerde QR's schuiven vanzelf door zonder dat iemand een timer schrijft. De helderheidsregeling, de tipmelding, de beeldvolgorde van de encoder en de stopvoorwaarden blijven allemaal die van upstream.
Wat werkt, en wat niet
Werkt: de volledige menuboom, seeds laden via QR of met de hand, passphrases, xpub exporteren, PSBT's laden en ondertekenen, SeedQR-back-up, instellingen, en elk scherm dat een QR tekent. Drie gesimuleerde kaarten gaan in en uit de lezer, kunnen met een pincode geïnitialiseerd worden en controleren die pincode wanneer erom gevraagd wordt, en elk slot kan een SeedKeeper of een Satochipzijn, jouw keuze voor je hem insteekt. Een gesimuleerde SeedKeeper bewaart een seed en geeft die daarna terug, en dat is de stap waarop de hardware eigenlijk verkocht wordt: zet de seed één keer op de kaart, en laad hem in de signer wanneer je hem nodig hebt.
Werkt niet, en dat weet je beter voor je gaat zoeken:
- Een seed op een Satochip zetten: door een fout in de firmware, niet in de simulator. De gesimuleerde Satochip houdt wel degelijk een seed bij en leidt er sleutels uit af, maar het importscherm van de wallet zelf gaat onderuit op het moment dat het lukt:
card_bip32_import_seed()geeft een publieke sleutel terug, en het scherm pakt die uit als drie waarden. Dat is een fout van upstream, aanwezig in de uitgebrachte firmware en nog altijd op de ontwikkeltak, dus echte hardware faalt op dezelfde manier. Het SeedKeeper-pad is niet getroffen, en dat is net het pad waarop het toestel verkocht wordt. - Geheimen van de ene kaart naar de andere kopiëren. Dat is een versleutelde uitwisseling van kaart naar kaart, en de simulator weigert die liever dan ze stilletjes in leesbare tekst te doen.
- microSD. Er is geen kaartsleuf om na te bootsen, dus alles wat daarlangs loopt ontbreekt: instellingen leven in een bestandssysteem in het geheugen en worden gewist wanneer je de pagina herlaadt, en de stappen voor firmware-updates gebeuren niet.
- Alles wat vanuit een achtergrondthread geanimeerd wordt. Geen draaiend wachtsymbool, geen lange tekst die voorbijschuift, geen pulserende waarschuwingsrand. De twee animaties die informatie dragen (het live camerabeeld en de geanimeerde QR-weergave) worden met de hand aangedreven. Functies die hun werk in een thread doen, zoals adresverificatie door alles af te gaan, lopen niet af.
- Gedrag op basis van timing. Geen wistimer, geen schermbeveiliging, geen batterijmetingen.
- Echte beveiligingseigenschappen. Uiteraard, en uitgebreid hieronder.
Wat je zelf kunt controleren
"Het is de echte firmware" is een bewering, en beweringen op een website zijn waard wat je ervoor betaald hebt. De reden dat deze wél iets waard is, is dat je ze kunt nagaan zonder ons ook maar iets te vragen.
De pin
De wallet is vastgepind in een bestand met de naam UPSTREAM op één commit van 3rdIteration/seedsigner, de smartcard-fork van SeedSigner. Op het moment van schrijven:
repo = https://github.com/3rdIteration/seedsigner.git
commit = 662d9dba2327eb77d6924ae9bd62d4902bf24634
tag = SeSi-0.8.7+ShSi-B11
wallet_zip_sha256 = 22e6034509b547242db58ea7303d3f605007c60e637444b173e505eabbb834ca
wallet_zip_contents_sha256 = d2527f58d122a7155343b215b596dc94991dad5e893b9f5ec30da6ffe1463684
Een gepubliceerde release tag, niet de tip van een tak, en dat om twee bepaalde redenen. Een taktip beweegt: rebase of force-push de ontwikkeltak en de commit-hash hierboven bestaat niet meer op de remote, waarna elke herbouw die hier beschreven staat blijft steken bij het ophalen, voor iedereen, definitief. Een tag is een naam die upstream gepubliceerd heeft en die niet beweegt. En net die tag is degene waaruit het officiële smartcard-image van het toestel gebouwd wordt, dus de code in de simulator en de code op het fysieke toestel zijn dezelfde code, niet louter gelijkaardige versies. De commit-hash blijft de pin; de tag zegt enkel welke release het is.
De reproduceerbare build
Geen van beide wallet-zips zit in de repository. Dat is bewust: een wallet die je gevraagd wordt te vertrouwen, kun je beter zelf herbouwen dan downloaden van een beheerder die zegt dat hij het juiste erin geplakt heeft. Een buildscript stelt hem samen vanaf de pin, en dat hele script bestaat opdat de hash die het afdrukt vergeleken kan worden met de hash van het bestand dat deze website je gaf.
Opdat die vergelijking iets zou betekenen, moet de build reproduceerbaar zijn, dus elke invoer is inhoudsgeadresseerd: upstream en de afhankelijkheden uit git via commit-hash, de PyPI-afhankelijkheden via de sha256 van hun artefact, de Pyodide-runtime via de sha256 van zijn release-tarball, en het ene meegeleverde bestand van derden, jsQR, via een checksum die je zelf tegen npm kunt controleren. De zip zelf wordt met de hand geschreven in plaats van door het zip commando, met tijdstempels vastgezet op de datum van de gepinde commit, rechten vastgezet ongeacht de umask van wie bouwt, entries in één canonieke gesorteerde volgorde in plaats van in de volgorde die het bestandssysteem toevallig teruggaf, nergens gecompileerde bytecode, geen symlinks, en een lege archiefopmerking. Niets van de buildmachine belandt in de uitvoer: geen paden, geen gebruikersnaam, geen umask, geen tijdzone, geen taalinstelling.
Wat "reproduceerbaar" echt vraagt
Dat laatste punt staat er door een echte fout, en die illustreert het probleem beter dan om het even hoeveel theorie.
De bewering hield stand op één machine en zou op die van iemand anders gebroken zijn. Een hulpfunctie die kiest welk licentiebestand er per afhankelijkheid meegaat, sorteerde haar kandidaten met een gewone sortering, en erfde zo de taalinstelling van de aanroeper, terwijl elke andere sortering in het script de collatievolgorde uitdrukkelijk vastlegde. Levert een afhankelijkheid meer dan één licentiebestand op dezelfde diepte, dan besliste de collatie welk bestand in de zip belandde. Twee mensen met verschillende taalinstellingen zouden verschillende bytes gekregen hebben. En, erger nog, verschillende inhouds hashes, wat net de terugval onderuit zou hebben gehaald die een verpakkingsverschil van een echt verschil moet onderscheiden.
Niemand zou het gemerkt hebben tot een onbekende een build probeerde te verifiëren en dat niet lukte, en de eerlijke lezing van zo'n mislukking van buitenaf is "de beheerder liegt". De oplossing was één regel. Ze vinden vergde een doorlichting die specifiek naar afhankelijkheid van de buildmachine zocht, en de gepubliceerde hashes verschoven daardoor: die hierboven komen uit de eerste build waarvan de uitvoer niet afhangt van waar ze draaide, byte voor byte geverifieerd onder een andere taalinstelling en een andere tijdzone. Reproduceerbaarheid is geen eigenschap die je verklaart, het is een eigenschap waarvoor je op zoek gaat naar tegenvoorbeelden.
De zlib-kanttekening, kort
De build drukt twee hashes af. De eerste is de sha256 van het zipbestand, en die vergelijk je met een download. De tweede is de sha256 van een manifest met een sha256 per bestand binnenin de zip.
Verschillen de ziphashes maar komt de inhoudshash overeen, dan bevatten de twee builds exact dezelfde bestanden en verschillen ze enkel in hoe goed ze gecomprimeerd werden: sommige distributies leveren zlib-ng mee, waarvan de deflate-uitvoer niet byte voor byte gelijk is aan die van standaard zlib. Dat is een verpakkingsverschil, geen codeverschil. En verschillen ook de inhoudshashes, dan vertelt het manifest je precies om welke bestanden het gaat, in plaats van je met één nutteloos bitje informatie achter te laten.
Wat wel en wat niet verdedigd wordt
Er is een echte lijst van dingen die dit project zorgvuldig doet, en het is de moeite om daar uitdrukkelijk over te zijn, en daarna even uitdrukkelijk over wat je daar allemaal niet mee koopt.
Wat echt van toepassing is:
- Cross-origin isolatie. Vereist om het geheel überhaupt te laten draaien, en het betekent ook dat het document niet ingebed kan worden in een losse zee van bronnen van derden.
- Bescherming tegen inbedding. bitsaga.be levert de simulator met
Content-Security-Policy: frame-ancestors 'none'enX-Frame-Options: DENY, zodat niemand hem in een iframe op zijn eigen site kan zetten en kan laten doorgaan voor iets anders. - Geen uitgaande verbindingen. Het contentbeveiligingsbeleid van de pagina laat enkel verzoeken naar dezelfde oorsprong toe, genoeg om de runtime en de wallet op te halen, en niets meer. jsQR wordt vanaf dezelfde oorsprong geleverd in plaats van vanaf een CDN, en dat wordt dubbel afgedwongen: een kopie op een CDN zou zowel door het isolatiebeleid als door de CSP geweigerd worden. Er is ook geen backend, dus zelfs een fout die iets zou willen versturen, heeft nergens om het naartoe te sturen.
- Er blijft niets achter. Instellingen leven in een bestandssysteem in het geheugen. Ook de staat van de gesimuleerde kaarten leeft in het geheugen, dus de pagina herladen geeft je elke keer drie fabrieksnieuwe blanco kaarten.
- De pin en de reproduceerbare build, hierboven beschreven, met elke afhankelijkheid vastgepind via hash in plaats van via een versienummer.
En dan nu de grens, en dat is geen kanttekening, dat is de hele vorm van het ding:
Niets daarvan maakt het veilig voor echte sleutels
En geen enkele hoeveelheid bijkomend techniekwerk zou dat kunnen. Er is geen secure element om een sleutel in te stoppen, geen air gap om hem achter te houden, en geen manier om van een JavaScript-omgeving op een computer voor algemeen gebruik een van beide te maken. Dit is een leer- en evaluatiemiddel. Het bevat niets van waarde en is daar ook niet voor gemaakt.
Het dreigingsmodel dat al het bovenstaande verdedigt, is "je kunt nagaan dat ik niet aan de firmware geknoeid heb". Het is niet, en kan nooit zijn, "je seed is hier veilig".
Die indrukwekkend klinkende lijst staat er net omdat de reproduceerbare build de ene bewering is die je beter controleert dan gelooft. Ze staat er niet om het tabblad als een toestel te laten aanvoelen. Heb je tot hier gelezen en denkt een deel van jou dat het waarschijnlijk wel meevalt om één keertje een echte seed in te voeren, ga dan terug en lees het kader bovenaan opnieuw, want dan heeft deze pagina gefaald.
Wat je nu kunt doen
Vier dingen, in volgorde van inspanning.
Probeer het. bitsaga.be/seedsigner-simulator, pijltjestoetsen en Enter, en een publieke testseed in plaats van een van jezelf. Een halfuurtje ermee vertelt je meer over of je het toestel wil dan eender welke recensie.
Gebruik het daarna ergens voor. Achter de simulator zit Bitsaga Signet, mijn eigen private bitcoin-testnetwerk met om de dertig seconden een blok en een faucet, zodat de multisig-tutorial na een paar minuten eindigt in een echte bevestiging in plaats van in een schermafbeelding. Het is geen netwerk waar je je eigen wallet mee kunt verbinden, en dat is ook niet de bedoeling: het bestaat om de tutorial te laten werken. De coins zijn geen echte bitcoin. Ze bestaan enkel op dat testnetwerk, kunnen niet verkocht of naar iemand gestuurd worden, en zijn niets waard.
Koop het echte toestel. Een simulator kan je de stappen tonen; hij kan niets ondertekenen waar je om zou geven. De smartcard-build ligt in de shop in zijn in 3D geprinte behuizing, geleverd met de SeedKeeper-kaarten waar hij rond gebouwd is. De versie in CNC-gefreesd aluminium is nog niet beschikbaar, maar komt binnenkort.
En ben je van het paranoïde soort (goed zo), herbouw dan de zip en controleer hem zelf. Kloon de repository, draai de build, en vergelijk de hash ervan met het bestand dat deze site je aanbiedt:
./build/build-wallet-zip.sh smartcard
sha256sum build/out/wallet-smartcard.zip
curl -s https://bitsaga.be/seedsigner-simulator/wallet-smartcard.zip | sha256sum
Komen die overeen, dan draaide je de gepinde boom van upstream, zijn gepinde afhankelijkheden en het gesimuleerde smartcardpakket van dit project, en niets anders. Komen ze niet overeen maar de inhoudshash wel, dan vond je een compressieprogramma, geen complot. En komen ze helemaal niet overeen, zeg dat dan luid: dat is net het faalgeval dat het hele buildscript zichtbaar moest maken.
De lange versie van al het bovenstaande staat in het architectuurdocument, de details over uitrollen in de gids voor zelf hosten, en de volledige inventaris van afhankelijkheden (versie, herkomst, licentie, en hoe je elk ervan controleert) in THIRD-PARTY.md.
Dit is een onafhankelijk project. Het is niet verbonden met en niet goedgekeurd door het SeedSigner-project, waarvan de firmware het interessante deel ervan is, en het draaien ervan bewijst niets over een echt toestel. Alles wat het doet, valt onder de MIT-licentie en staat op GitHub. Vond je er iets mis mee (in de code of op deze pagina), laat het ons weten.
