Kort samengevat. Een wallet-auditor bestempelde de desktopwallet van Nunchuk als niet-reproduceerbaar, wat betekent dat niemand buiten het bedrijf kon aantonen dat de download overeenkwam met de gepubliceerde broncode, en wij bevestigden elk cijfer in dat rapport zelf. Nunchuk loste het daarna binnen twee dagen op, en onze eigen herbouw van de nieuwe versie komt in 1.584 van de 1.587 bestanden overeen met die van hen, waarbij de enige verschillen 1.302 ingebedde klokstanden zijn en geen enkele regel code.
De korte versie
Als je een bitcoinwallet downloadt, download je een programma dat iemand anders heeft gecompileerd. Dat de broncode op GitHub staat, bewijst niet dat die twee iets met elkaar te maken hebben. Iemand kan schone code publiceren en jou iets anders opsturen, en jij hebt geen enkele manier om dat te merken.
De oplossing daarvoor heet een reproduceerbare build. Als jij de gepubliceerde broncode zelf kunt compileren en er een bestand uit komt dat identiek is aan dat op de downloadpagina, dan is aantoonbaar dat de download uit die broncode komt. Lukt dat niet, dan vertrouw je het bedrijf en niet de code.
WalletScrutiny is een project dat precies dit probeert, wallet voor wallet, en het resultaat publiceert. Op 18 augustus publiceerden zij een rood oordeel voor Nunchuk Desktop 2.6.4: niet reproduceerbaar. Dat was het negende oordeel van dit soort over dezelfde wallet, in een draad die al sinds april 2025 liep.
Wij deden drie dingen. We controleerden of het rapport klopte. Dat deed het, tot in elk detail. Vervolgens ontdekten we dat Nunchuk het onderliggende probleem stilletjes had opgelost op 19 en 20 augustus, uren na het laatste rapport. En daarna compileerden we versie 2.6.6 zelf, op onze eigen machine, en vergeleken die met de officiële download.
Van die drie zijn er twee hetzelfde bestand dat twee keer is meegeleverd. En de verschillen erin zijn geen code. Het zijn 1.302 vastgelegde tijdstempels: het moment waarop elk bestand toevallig op de bouwmachine werd gezet. Onze kopie en die van hen zijn 58 minuten na elkaar gemaakt, en dat is het hele meningsverschil.
Wat dit voor jou betekent. Nunchuk Desktop op Linux is nog niet perfect reproduceerbaar, maar het scheelt heel weinig, en het resterende gat is boekhouding en niet iets waarin kwaadaardige code zich kan verstoppen. De versie ervoor, 2.6.5, werd één dag eerder gecontroleerd en verschilde op 157 punten. Dat is een echte verbetering in zeer korte tijd.
Drie waarschuwingen als je deze wallet gebruikt. Versie 2.6.6 staat gemarkeerd als pre-release en wordt, anders dan elke versie ervoor, geleverd zonder handtekeningbestand, dus er is op dit moment niets om de download tegen te controleren. Het is alleen Linux, dus gebruikers van macOS en Windows hebben hier nog niets aan. En 2.6.5, de huidige gewone release, staat nog steeds bij de versies die als niet-reproduceerbaar zijn gemarkeerd.
Alles hieronder is het rekenwerk: de exacte commando's, de exacte cijfers, en vier kleine correcties die wij getest hebben en die het gat volledig zouden dichten. Sla het over, tenzij je ons wilt controleren.
De technische details
1. Klopt het rapport zelf?
Voordat we Nunchuk controleerden, controleerden we degene die Nunchuk controleert. Elk cijfer in verificatie bcf39301… is hier vanaf nul nagerekend.
| Bewering in het rapport | Ons resultaat |
|---|---|
nunchuk-linux-v2.6.4.zip is 5d7b4c36…ae26 | komt overeen |
Die hash komt overeen met de release SHA256SUMS | komt overeen |
SHA256SUMS.asc is een geldige handtekening van 8C8ECD3F…ED2EF525 | geldige handtekening, sleutel is Ta Tat Tai (Nunchuk binary release signing key) |
AppImage erin, 181.107.192 bytes, 87ebfd10…b195 | byte-exacte overeenkomst |
Het rapport klopt. We vonden één punt dat het te zwak formuleert. SHA256SUMS.asc is geen losstaande handtekening over SHA256SUMS; het is een clearsigned kopie die de sommen zelf meedraagt. Het commando waar de meeste mensen naar grijpen, mislukt:
$ gpg --verify SHA256SUMS.asc SHA256SUMS
gpg: not a detached signature
Je moet de ondertekende inhoud eruit halen en die vergelijken met het gewone bestand. Dat deden wij, ze zijn identiek, dus de keten houdt stand. Maar wie zijn intuïtie volgt, loopt tegen een foutmelding aan en kan concluderen dat de handtekening kapot is terwijl er niets mis mee is. Een losstaande .sig ernaast zou niets kosten.
2. Is een Nunchuk-desktopbuild ooit reproduceerbaar geweest?
Niet volgens de administratie. De Nunchuk-desktoppagina toont negen verificaties, allemaal van dezelfde verificateur, en bij elke staat Niet reproduceerbaar: 1.9.47, 1.9.48, 1.9.49, 2.5.0, 2.6.0, 2.6.2, 2.6.3, 2.6.4 en 2.6.5. Bij geen enkele staat Reproduceerbaar bij controle. Nog één 1.9.48-bestand staat vermeld zonder enige verificatie, en vijf oudere regels zitten achter een knop "Show 5 more" die wij niet hebben uitgeklapt, dus daarover kunnen wij niets zeggen.
De geschreven neerslag loopt ernaast in nunchuk-desktop issue #86, geopend op 10 april 2025 door dezelfde tester (xrviv op GitHub, dannybuntu op WalletScrutiny) en inmiddels vijftien reacties lang. Het openingsrapport gaat niet eens over hashes: de build kon niet starten, omdat contrib/libnunchuk verwees naar een privé GitLab-repository die buitenstaanders helemaal niet konden klonen.
Vanaf 2.6.2 werden de rapporten forensisch. De herbouw verschilde van de release in een vast patroon van 85 posten: 6 bestanden anders, 3 bibliotheken alleen in de release, 76 bibliotheken alleen in de herbouw. Diezelfde 85 kwamen terug bij 2.6.3 en opnieuw bij 2.6.4. De reactie van 19 augustus stelde daarna de vraag waar het om draaide, nadat twee eerdere vragen onbeantwoord waren gebleven:
Wat is er tussen v2.6.4 en v2.6.5 veranderd aan de Linux-releaseverpakking? Er is niets in
reproducible-builds/veranderd [...] en toch is het uitgebrachte bestand ingrijpend anders. Iets buiten die map maakt de release, en wat dat ook is, het is tussen die twee tags verschoven.
3. Wat er op 19 en 20 augustus gebeurde
Versie 2.6.6 werd op 20 augustus 2026 om 05:54 UTC gepubliceerd. Ons eerste commando in deze controle liep om 06:04 UTC. De release was tien minuten oud, en daarom documenteren wij uiteindelijk een lopende oplossing in plaats van een blijvende fout.
| Wanneer (UTC) | Wat |
|---|---|
| 18 aug 08:22 | rapport over 2.6.4, drie openstaande vragen |
| 19 aug 10:30 | rapport over 2.6.5, "welke workflow maakt de release-ZIP eigenlijk?" |
| 19 aug | vier commits die reproducible-builds/herzien, en build-linux.yml wordt verwijderd |
| 20 aug 05:09 | tag 2.6.6, commit 025aba7, bericht "Reproducibile build" |
| 20 aug 05:54 | 2.6.6 gepubliceerd |
Welke workflow maakte de release-ZIP? Bij tag 2.6.4 publiceerde geen van beide publieke workflows iets naar een release. build-linux.yml liep alleen op handmatige trigger; die bouwde wel een ZIP, maar uploadde alleen de AppImage als CI-artefact, en die ZIP zit om een map heen terwijl de uitgebrachte ZIP de AppImage in de hoofdmap heeft. reproducible_linux.yml liep op tags maar uploadde eveneens alleen de AppImage. Geen enkele publieke workflow zette dus nunchuk-linux-v2.6.4.zip op de releasepagina. Iets buiten de repository deed dat. Wij kunnen niet zien wat, en dat is precies het punt: de herbouw werd vergeleken met een bestand waarvan het recept niet gepubliceerd was.
Kan de release de reproduceerbare build publiceren? Sinds tag 2.6.6 wel. reproducible_linux.yml is weg. De nieuwe build-linux.yml start bij het pushen van een tag, bouwt binnen de gedocumenteerde Docker-image via build_linux.sh, en eindigt met gh release create dat precies die ZIP uploadt. Het uitgebrachte bestand is nu het CI-bestand.
En de OAuth-waarden uit de compilatie? Bij 2.6.4 compileerde CMakeLists.txt de waarden OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET en OAUTH_REDIRECT_URI in het binaire bestand met add_definitions, gevoed vanuit repository-secrets. Of de releasebuilds daadwerkelijk niet-lege waarden hadden, kunnen wij van buitenaf niet zien, en dat was ook het punt van de tester: als ze gezet waren, kon geen enkele buitenstaander het binaire bestand ooit evenaren. Bij 2.6.6 is het mechanisme verdwenen. De drie definities ontbreken in CMakeLists.txt, ontbreken in de workflow, en de teksten ontbreken in het uitgeleverde binaire bestand, dat nu dynamisch linkt naar libQt5NetworkAuth.so.5 . De vraag is niet zozeer beantwoord als wel overbodig geworden, en dat is de betere uitkomst.
Je kunt de verandering in de uitgeleverde bestanden zien zonder ook maar één regel code te lezen:
| 2.6.4 uitgebracht | 2.6.6 uitgebracht | |
|---|---|---|
| meegeleverde bibliotheken | 62 | 136 |
bin/nunchuk-qt | 167.157.224 bytes | 117.127.872 bytes |
De officiële 2.6.6 levert 77 bibliotheken mee die 2.6.4 niet had, en heeft alle drie de bibliotheken laten vallen die alleen de oude officiële build meedroeg (libcrypto.so.3, libssl.so.3, libpq.so.5). Die drie zijn precies de bibliotheken die de tester meldde als alleen aanwezig in de release. De officiële download heeft de vorm aangenomen die een herbouw vanuit broncode oplevert.
4. Een detail waar niemand naar vroeg: hwi
bin/hwi was een van de zes bestanden die altijd verschilden. Het wordt niet uit broncode gecompileerd; het wordt tijdens het inpakken gedownload, dus keken we waarvandaan.
Bij tag 2.6.4 haalde package_linux.sh HWI 3.1.0 op bij giahuy98/HWI, een persoonlijke fork, met de officiële regel bitcoin-core/HWI er direct boven uitgecommentarieerd, en zonder checksum.
De hwi die daadwerkelijk in de release 2.6.4 zat, heeft hash e8669f2f…d05b4. Wij downloadden HWI 3.2.0-displayaddress bij nogibi/HWI, een andere fork op een andere versie, en die komt exact overeen. De uitgebrachte wallet leverde dus een hardwarewallet-koppelbestand mee dat het eigen publieke buildscript niet had kunnen maken.
Bij 2.6.6 pint het script de nogibi-release vast en controleert het de SHA-256 voordat het bestand wordt geïnstalleerd, en dat is het bestand dat wordt meegeleverd. Dat specifieke gat is gedicht, al blijft de afhankelijkheid een fork in plaats van de bron.
5. Onze eigen herbouw van 2.6.6
Wij volgden reproducible-builds/README.md precies: klonen, git checkout 2.6.6, submodules, de Docker-image bouwen, de build draaien. De builder-image kostte ongeveer 18 minuten, de applicatiebuild 16 minuten en 48 seconden op 12 cores.
Onze AppImage kwam uit op 184.678.904 bytes. De officiële is 184.678.904 bytes. Even groot tot op de byte, andere SHA-256. Dus pakten we beide SquashFS-bomen uit en vergeleken we elk bestand.
| Bestand | Afwijkende bytes | Van |
|---|---|---|
bin/nunchuk-qt | 3,894 | 117,127,872 |
lib/libcrypto.so | 23 | 3,417,040 |
lib/libcrypto.so.1.1 | 23 | identieke kopie van het bovenstaande |
| de andere 1.584 bestanden | 0 |
3.940 bytes van de 184.678.904, oftewel 0,0021 procent. De bestandslijsten komen exact overeen: dezelfde 1.587 bestanden, dezelfde paden, aan geen van beide kanten iets extra's.
libcrypto.so: 23 bytes
Negentien daarvan zitten in de 20-byte GNU build-ID, die een hash is over de rest van het bestand en dus verandert zodra er iets anders verandert. De andere vier staan hier:
ours: built on: Thu Aug 20 06:20:27 2026 UTC
official: built on: Thu Aug 20 03:47:21 2026 UTC
OpenSSL stempelt zijn eigen compilatietijd in de bibliotheek. Vier cijfers van een klok, en verder niets in drieënhalve megabyte.
nunchuk-qt: 3.894 bytes
Twintig zijn de build-ID. De andere 3.874 zitten allemaal in .rodata, en ze laten zich lezen als 64-bits tijdstempels in milliseconden. Het zijn er 1.300, en elk daarvan verschilt van de officiële build met precies dezelfde 3.477 seconden.
off=79,719,888 ours=2026-08-20 06:07:52.934 official=2026-08-20 05:09:55.086
off=79,719,954 ours=2026-08-20 06:07:52.934 official=2026-08-20 05:09:55.086
off=79,719,976 ours=2026-08-20 06:07:52.708 official=2026-08-20 05:09:54.970
delta(seconds) -> count: {3477: 1300}
Dat is de resource-compiler van Qt. Wanneer rcc een QML-bestand of een pictogram in het binaire bestand opneemt, legt het de laatste wijzigingsdatum van dat bestand ernaast vast. git clone zet die tijden op het moment waarop jij hebt gekloond. Zo dragen 1.300 onderdelen het moment waarop de repository is uitgechecked, en verschillen de twee builds precies de 58 minuten die tussen hun checkouts zaten.
Geen enkele byte gecompileerde code verschilt, en de ELF-sectie-indeling is identiek. De twee binaire bestanden zijn hetzelfde programma dat het oneens is over hoe laat het was.
6. Vier correcties, elk getest
We hebben elk van deze correcties binnen de eigen builder-image van Nunchuk uitgevoerd in plaats van erover te redeneren.
1. Zet SOURCE_DATE_EPOCH in build_linux.sh. De rcc van Qt 5.15.2 respecteert de variabele. Dezelfde resource, twee verschillende bestandstijden:
without SOURCE_DATE_EPOCH: 786b4df4… 2b229e45… different
with SOURCE_DATE_EPOCH: 69277d4f… 69277d4f… identical
Dat verwijdert alle 1.300 tijdstempels. Nunchuk doet dit al op Windows, waar windows.sh de regel export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)bevat. Het Linux-pad heeft die regel nooit gekregen.
2. Zet de variabele ook in Dockerfile.linux , voordat OpenSSL wordt gecompileerd. OpenSSL 1.1.1g leest hem rechtstreeks, in util/mkbuildinf.pl regel 15:
my $date = gmtime($ENV{'SOURCE_DATE_EPOCH'} || time()) . " UTC";
Dat verwijdert de regel built on: .
3. Geef twee opties door aan mksquashfs. appimagetool negeert SOURCE_DATE_EPOCH en stempelt hoe dan ook een verse bouwtijd in de SquashFS, zelfs wanneer de invoerbestanden identieke bestandstijden hebben. Twee runs met drie seconden ertussen:
appimagetool ... 2bdd2d6f… f12f50e1… different
appimagetool --mksquashfs-opt -mkfs-time \
--mksquashfs-opt 0 \
--mksquashfs-opt -all-time \
--mksquashfs-opt 0 ... c453166f… c453166f… identical
4. Zet de bestandstijd van de AppImage zelf vast voordat je hem inpakt. De ZIP legt die vast, dus zonder dit verandert het archief elke keer, ook als de inhoud gelijk blijft:
7z with a fresh mtime: 3a70b78d… 61b6c72e… different
7z with a pinned mtime: 3ca298f8… 3ca298f8… identical
Punt 3 weegt het zwaarst voor de documentatie. README.md zegt op dit moment dat je jouw ZIP moet diff tegen de officiële, en die test kan vandaag niet slagen wat iemand ook doet, want hij meet de klok en niet de build.
7. Wat er nog niet klopt
- 2.6.6 is een pre-release en is niet ondertekend. Elke release van 2.5.0 tot en met 2.6.5 leverde
SHA256SUMSenSHA256SUMS.ascmee. 2.6.6 levert geen van beide. De geautomatiseerde pijplijn won reproduceerbaarheid en verloor onderweg de GPG-handtekening. Allebei zijn ze het waard. - Alleen Linux. macOS en Windows staan in de README als "standby", en 2.6.6 levert geen van beide.
- 2.6.5 is nog steeds de huidige volledige release, en het is een van de versies die als niet-reproduceerbaar zijn gemarkeerd.
- De builder-image is niet vastgepind.
apt installzonder versies, enappimagetooldat wordt opgehaald uit decontinuousrelease, zorgen ervoor dat de container meebeweegt met de buitenwereld. Qt, qtkeychain, olm, CQtDeployer en OpenSSL zijn allemaal netjes vastgepind. Die twee niet. - De overgebleven afhankelijkheid is een fork.
hwikomt nu met een checksum, maar vannogibi/HWIin plaats vanbitcoin-core/HWI.
Met dank
dannybuntu van WalletScrutiny diende zestien maanden lang geduldige, precieze en reproduceerbare rapporten in, meestal in stilte, en het laatste bracht een walletbedrijf binnen twee dagen in beweging. Nunchuk heeft het, toen ze eenmaal reageerden, niet toegedekt: ze haalden de workflow weg die het niet-verifieerbare bestand maakte, lieten de tagbuild de releasebuild worden, haalden het mechanisme voor compilatiegeheimen eruit, en zetten een checksum op de ene afhankelijkheid die ze blind binnenhaalden. Dat is de juiste reactie en die is zeldzamer dan zou moeten.
De vier correcties hierboven zijn openbaar en na te rekenen, dus iedereen die ze wil, Nunchuk incluis, kan ze hier zo overnemen.
Dit is een onafhankelijke controle. Wij zijn niet verbonden aan Nunchuk of WalletScrutiny en wij verkopen geen Nunchuk-producten. Alles hierboven is op 20 augustus 2026 op onze eigen machine uitgevoerd tegen de publieke releases en de publieke repository, en elke hash hier is na te rekenen met de commando's in Nunchuks eigen reproducible-builds/README.md. Als wij iets fout hebben, horen wij dat liever wel dan niet.