---
title: "Nunchuk is 3,940 bytes from reproducible · Bitsaga Insights"
description: "WalletScrutiny reported Nunchuk Desktop as not reproducible. We checked every figure in that report, then rebuilt the version that shipped ten minutes before we started. Out of 1,587 files, 3 differ, and every differing byte is a clock reading."
url: "https://bitsaga.be/insights/nunchuk-reproducible-build"
language: "en"
---

> WalletScrutiny reported Nunchuk Desktop as not reproducible. We checked every figure in that report, then rebuilt the version that shipped ten minutes before we started. Out of 1,587 files, 3 differ, and every differing byte is a clock reading.
[Insights](https://bitsaga.be/insights) · Security

# Nunchuk is 3,940 bytes from reproducible

By [Rob Segers](https://bitsaga.be/about) · August 20, 2026

> **TL;DR.** A wallet auditor flagged Nunchuk's desktop wallet as not reproducible, meaning nobody outside the company could prove the download matched the published source, and we confirmed every figure in that report ourselves. Nunchuk then fixed it within two days, and our own rebuild of the new version matches theirs in 1,584 of 1,587 files, with the only differences being 1,302 embedded clock readings rather than any actual code.

## The short version

When you download a bitcoin wallet, you download a program somebody else compiled. The source code being on GitHub does not prove the two are related. Somebody could publish clean code and ship you something different, and you would have no way to tell.

The fix for this is called a **reproducible build**. If you can compile the published source yourself and end up with a file identical to the one on the download page, then the download provably came from that source. If you cannot, you are trusting the company, not the code.

[WalletScrutiny](https://walletscrutiny.com) is a project that tries exactly this, wallet by wallet, and publishes the result. On 18 August they published a red verdict for Nunchuk Desktop 2.6.4: **not reproducible**. That was the ninth such verdict on the same wallet, on a thread that had been running since April 2025.

We did three things. We checked whether the report was correct. It was, in every detail. We then found that Nunchuk had quietly fixed the underlying problem on 19 and 20 August, hours after the most recent report. And then we compiled version 2.6.6 ourselves, on our own machine, and compared it to the official download.

3 of 1,587

files differ between our own build and Nunchuk's official download

Of the three, two are the same file included twice. And the differences inside them are not code. They are 1,302 recorded timestamps: the moment each file happened to be copied onto the machine that built it. Our copy and their copy were made 58 minutes apart, and that is the entire disagreement.

**What this means for you.** Nunchuk Desktop on Linux is not yet perfectly reproducible, but it is very close, and the remaining gap is bookkeeping rather than anything that could hide malicious code. The version before it, 2.6.5, was checked one day earlier and differed in 157 places. That is a real improvement in a very short time.

Three cautions if you use this wallet. Version 2.6.6 is marked as a pre-release and, unlike every version before it, **ships without a signature file**, so there is currently nothing to check the download against. It is Linux only, so macOS and Windows users get none of this yet. And 2.6.5, the current normal release, is still one of the versions marked not reproducible.

Everything below is the working: the exact commands, the exact numbers, and four small fixes we tested that would close the gap completely. Skip it unless you want to check us.

---

## The technical detail

### 1. Is the report itself correct?

Before checking Nunchuk we checked the person checking Nunchuk. Every figure in [verification `bcf39301…`](https://walletscrutiny.com/verifier/?pubkey=1f9e547c2f31942623b8ad1d07713282e8640fd8cf474e9f79f18ace8af216ed#verificationId=bcf393011779767d6a76af7220c0fd6532861d95a2506dd50a38bc2a6861f265) was reproduced here from scratch.

| Claim in the report | Our result |
|---|---|
| `nunchuk-linux-v2.6.4.zip` is `5d7b4c36…ae26` | matches |
| That hash matches the release `SHA256SUMS` | matches |
| `SHA256SUMS.asc` is a good signature by `8C8ECD3F…ED2EF525` | good signature, key is *Ta Tat Tai (Nunchuk binary release signing key)* |
| AppImage inside, 181,107,192 bytes, `87ebfd10…b195` | byte-exact match |

The report is accurate. We found one thing it understates. `SHA256SUMS.asc` is not a detached signature over `SHA256SUMS`; it is a clearsigned copy carrying the sums inside itself. The command most people would reach for fails:

```
$ gpg --verify SHA256SUMS.asc SHA256SUMS
gpg: not a detached signature
```

You have to extract the signed payload and diff it against the plain file. We did, they are identical, so the chain holds. But anyone following their instincts hits an error and may conclude the signature is broken when it is fine. A detached `.sig` alongside would cost nothing.

### 2. Has any Nunchuk desktop build ever been reproducible?

Not according to the record. The [Nunchuk desktop page](https://walletscrutiny.com/desktop/nunchuk/) lists nine verifications, all by the same verifier, and every one reads **Not reproducible**: 1.9.47, 1.9.48, 1.9.49, 2.5.0, 2.6.0, 2.6.2, 2.6.3, 2.6.4 and 2.6.5. Not one of them reads Reproducible when tested. A second 1.9.48 artifact is listed with no verification at all, and five older rows sit behind a "Show 5 more" control that we did not expand, so we cannot speak for those.

The written record runs alongside it in [nunchuk-desktop issue #86](https://github.com/nunchuk-io/nunchuk-desktop/issues/86), opened 10 April 2025 by the same tester (`xrviv` on GitHub, `dannybuntu` on WalletScrutiny) and now fifteen comments long. The opening report is not even about hashes: the build could not start, because `contrib/libnunchuk` pointed at a private GitLab repository that outsiders could not clone at all.

By 2.6.2 the reports had become forensic. The rebuild differed from the release in a stable pattern of 85 entries: 6 files different, 3 libraries only in the release, 76 libraries only in the rebuild. The same 85 came back at 2.6.3 and again at 2.6.4. The 19 August comment then asked the question that mattered, after two earlier ones went unanswered:

> **What changed in the Linux release packaging between v2.6.4 and v2.6.5?** Nothing in `reproducible-builds/` changed [...] yet the released artifact changed substantially. Something outside that directory produces the release, and whatever it is, it moved between those two tags.

### 3. What happened on 19 and 20 August

Version 2.6.6 was published at 05:54 UTC on 20 August 2026. Our first command in this check ran at 06:04 UTC. The release was ten minutes old, which is why we ended up documenting a fix in progress rather than a standing failure.

| When (UTC) | What |
|---|---|
| 18 Aug 08:22 | 2.6.4 report, three open questions |
| 19 Aug 10:30 | 2.6.5 report, "which workflow actually produces the release ZIP?" |
| 19 Aug | four commits reworking `reproducible-builds/`, and `build-linux.yml` is **deleted** |
| 20 Aug 05:09 | tag `2.6.6`, commit `025aba7`, message "Reproducibile build" |
| 20 Aug 05:54 | 2.6.6 published |

**Which workflow produced the release ZIP?** At tag 2.6.4, neither of the two public ones published anything to a release. `build-linux.yml` ran on manual trigger only; it did build a ZIP, but it uploaded only the AppImage as a CI artifact, and its ZIP wraps a folder while the released ZIP holds the AppImage at the root. `reproducible_linux.yml` ran on tags but also uploaded only the AppImage. So no public workflow put `nunchuk-linux-v2.6.4.zip` on the releases page. Something outside the repository did. We cannot see what, and that is the point: the rebuild was being compared against a file whose recipe was not published.

**Can the release publish the reproducible build?** As of tag 2.6.6, yes. `reproducible_linux.yml` is gone. The new `build-linux.yml` fires on a tag push, builds inside the documented Docker image via `build_linux.sh`, and ends with `gh release create` uploading exactly that ZIP. The released file is now the CI file.

**What about the compile-time OAuth values?** At 2.6.4, `CMakeLists.txt` compiled `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET` and `OAUTH_REDIRECT_URI` into the binary using `add_definitions`, fed from repository secrets. Whether the release builds actually had non-empty values we cannot tell from outside, and that was the tester's point too: if they were set, no outsider could ever match the binary. At 2.6.6 the mechanism is gone. The three definitions are absent from `CMakeLists.txt`, absent from the workflow, and the strings are absent from the shipped binary, which now links `libQt5NetworkAuth.so.5` dynamically instead. The question is moot rather than answered, which is the better outcome.

You can see the change in the shipped files without reading any code:

| | 2.6.4 released | 2.6.6 released |
|---|---|---|
| bundled libraries | 62 | 136 |
| `bin/nunchuk-qt` | 167,157,224 bytes | 117,127,872 bytes |

The official 2.6.6 bundles 77 libraries that 2.6.4 did not, and has dropped all three that only the old official build carried (`libcrypto.so.3`, `libssl.so.3`, `libpq.so.5`). Those three are exactly the ones the tester reported as present only in the release. The official download has moved to the shape a from-source rebuild produces.

### 4. A detail nobody asked about: hwi

`bin/hwi` was one of the six files that always differed. It is not compiled from source; it is downloaded during packaging, so we looked at where from.

At tag 2.6.4, `package_linux.sh` fetched HWI **3.1.0** from `giahuy98/HWI`, a personal fork, with the official `bitcoin-core/HWI` line commented out directly above it, and no checksum.

The `hwi` actually shipped inside the 2.6.4 release hashes to `e8669f2f…d05b4`. We downloaded HWI **3.2.0-displayaddress** from `nogibi/HWI`, a different fork at a different version, and it matches exactly. So the published wallet shipped a hardware-wallet interface binary that its own public build script would not have produced.

At 2.6.6 the script pins the nogibi release **and verifies its SHA-256 before installing it**, and that is the file shipped. That specific gap is closed, though the dependency is still a fork rather than upstream.

### 5. Our own rebuild of 2.6.6

We followed `reproducible-builds/README.md` exactly: clone, `git checkout 2.6.6`, submodules, build the Docker image, run the build. The builder image took about 18 minutes, the application build 16 minutes 48 seconds on 12 cores.

Our AppImage came out at **184,678,904 bytes**. The official one is **184,678,904 bytes**. Same size to the byte, different SHA-256. So we unpacked both SquashFS trees and compared every file.

| File | Bytes differing | Of |
|---|---|---|
| `bin/nunchuk-qt` | 3,894 | 117,127,872 |
| `lib/libcrypto.so` | 23 | 3,417,040 |
| `lib/libcrypto.so.1.1` | 23 | identical copy of the above |
| **the other 1,584 files** | **0** | |

3,940 bytes out of 184,678,904, or 0.0021 percent. The file lists match exactly: same 1,587 files, same paths, no extras on either side.

#### libcrypto.so: 23 bytes

Nineteen sit inside the 20-byte GNU build ID, which is a hash over the rest of the file and therefore changes whenever anything else does. The other four are here:

```
ours:     built on: Thu Aug 20 06:20:27 2026 UTC
official: built on: Thu Aug 20 03:47:21 2026 UTC
```

OpenSSL stamps its own compile time into the library. Four digits of a clock, and nothing else in three and a half megabytes.

#### nunchuk-qt: 3,894 bytes

Twenty are the build ID. The other 3,874 are all inside `.rodata`, and they decode as 64-bit millisecond timestamps. There are **1,300 of them**, and every single one is offset from the official build by exactly the same **3,477 seconds**.

```
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}
```

That is Qt's resource compiler. When `rcc` embeds a QML file or an icon into the binary, it records that file's last-modified time next to it. `git clone` sets those times to whenever you cloned. So 1,300 assets carry the moment the repository was checked out, and the two builds differ by the 58 minutes between their checkouts.

Not one byte of compiled code differs, and the ELF section layout is identical. The two binaries are the same program disagreeing about what time it was.

### 6. Four fixes, each tested

We ran each of these inside Nunchuk's own builder image rather than reasoning about them.

**1. Set `SOURCE_DATE_EPOCH` in `build_linux.sh`.** Qt 5.15.2's `rcc` honours it. Same resource, two different file mtimes:

```
without SOURCE_DATE_EPOCH:  786b4df4…   2b229e45…   different
with SOURCE_DATE_EPOCH:     69277d4f…   69277d4f…   identical
```

That removes all 1,300 timestamps. Nunchuk already does this on Windows, where `windows.sh` reads `export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)`. The Linux path never got the line.

**2. Set it in `Dockerfile.linux` too,** before OpenSSL is compiled. OpenSSL 1.1.1g reads it directly, in `util/mkbuildinf.pl` line 15:

```
my $date = gmtime($ENV{'SOURCE_DATE_EPOCH'} || time()) . " UTC";
```

That removes the `built on:` line.

**3. Pass two options through to `mksquashfs`.** `appimagetool` ignores `SOURCE_DATE_EPOCH` and stamps a fresh build time into the SquashFS regardless, even when the input files have identical mtimes. Two runs three seconds apart:

```
appimagetool ...                              2bdd2d6f…   f12f50e1…   different
appimagetool --mksquashfs-opt -mkfs-time \
             --mksquashfs-opt 0 \
             --mksquashfs-opt -all-time \
             --mksquashfs-opt 0 ...           c453166f…   c453166f…   identical
```

**4. Pin the AppImage's own mtime before zipping it.** The ZIP records it, so without this the archive changes every time even when its contents do not:

```
7z with a fresh mtime:   3a70b78d…   61b6c72e…   different
7z with a pinned mtime:  3ca298f8…   3ca298f8…   identical
```

Point 3 matters most for the documentation. `README.md` currently tells you to `diff` your ZIP against the official one, and **that test cannot pass today** no matter what anyone does, because it is measuring the clock rather than the build.

### 7. What is still not right

- **2.6.6 is a pre-release and it is unsigned.** Every release from 2.5.0 through 2.6.5 shipped `SHA256SUMS` and `SHA256SUMS.asc`. 2.6.6 ships neither. The automated pipeline gained reproducibility and lost the GPG signature on the way. Both are worth having.

- **Linux only.** macOS and Windows are marked "standby" in the README, and 2.6.6 ships neither.

- **2.6.5 is still the current full release,** and it is one of the versions marked not reproducible.

- **The builder image is not pinned.** `apt install` without versions, and `appimagetool` pulled from the `continuous` release, mean the container drifts with the outside world. Qt, qtkeychain, olm, CQtDeployer and OpenSSL are all pinned properly. Those two are not.

- **The remaining dependency is a fork.** `hwi` now comes with a checksum, but from `nogibi/HWI` rather than `bitcoin-core/HWI`.

## Credit

dannybuntu at WalletScrutiny filed patient, precise, reproducible reports for sixteen months, mostly into silence, and the last one moved a wallet company inside two days. Nunchuk, once they engaged, did not paper over it: they removed the workflow that was producing the unverifiable file, made the tag build the release build, took the compile-time secret mechanism out, and put a checksum on the one dependency they were downloading blind. That is the right response and it is rarer than it should be.

The four fixes above are public and reproducible, so anyone who wants them, Nunchuk included, can take them straight from here.

---

This is an independent check. We are not affiliated with Nunchuk or WalletScrutiny and we do not sell Nunchuk products. Everything above was run on our own machine on 20 August 2026 against the public releases and the public repository, and every hash here can be reproduced with the commands in Nunchuk's own `reproducible-builds/README.md`. If we got something wrong we would rather hear it than not.
