Verifying a download
A reproducible build asserts this came from this source, and anyone can check — which is the assurance an anonymously published project can offer in place of a code-signing certificate. Here is how to collect on it.
Three checks, and they answer different questions. Run them in this order; each one is worth less without the one before it.
| Check | What it proves | What it does not |
|---|---|---|
| Checksum | The file was not altered in transit. | Anything about who produced it, or what is in it. |
| Signature | The checksum list came from the holder of the project key. | That the key belongs to anyone you have reason to trust. |
| Rebuild | The binary contains this source and nothing else. | That the source is honest — read it, or read the reviews. |
The checksum#
curl -fsSLO https://zycord.com/releases//download/v<version>/SHA256SUMS.randomx
sha256sum --check --ignore-missing SHA256SUMS.randomx
# on macOS:
shasum -a 256 --check --ignore-missing SHA256SUMS.randomx
--ignore-missing is not optional, and not leniencyReleases now ship only the -randomx archives, so
SHA256SUMS.randomx is the list that covers what you downloaded;
SHA256SUMS.deb and SHA256SUMS.desktop cover the Debian packages and the
desktop wallet. Each covers several files and you downloaded one. Without the flag, the five you do not have are reported as
FAILED open or read and the command exits non-zero for a perfectly good download, on
the very page that tells you a mismatch means a compromised binary. A check whose normal result is
a failure is a check people learn to ignore.
The signature#
Releases currently ship zycord-release-key.asc — the public key — but
nothing signed with it beyond the updater's manifest: none of the checksum files has a detached
signature, and none is clearsigned. The gpg --verify below therefore fails
today with a missing file, and that is the release's gap rather than your mistake. Until it is closed, the
check that actually carries the trust argument is the
rebuild: it needs no signature at all, because you produce the bytes yourself and compare.
When a checksum signature is published it is made with the project key. The full fingerprint is:
E724 39CE DD85 11F9 D607 550B 87FD 60D5 EB4A 0B29
It is published in the whitepaper header, in the archived deposit, and in the genesis announcement. It never rotates silently: a key that changes without a signed statement from the old one is indistinguishable from a compromise, and should be treated as one.
Get the key from wherever is convenient and then check what you got against the line above. A key file whose fingerprint is that one is the project key no matter which host handed it to you, and one whose fingerprint is anything else is worthless no matter how trustworthy the host looked.
Three places carry it. Any of them will do:
# 1. The repository, if you have a clone. No network, no keyserver.
gpg --import packaging/zycord-release-key.asc
# 2. The release page, as an asset beside the archives.
curl -fsSLO https://zycord.com/releases//download/v<version>/zycord-release-key.asc
gpg --import zycord-release-key.asc
# 3. A keyserver. Use this one.
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys E72439CEDD8511F9D607550B87FD60D5EB4A0B29
Then confirm what entered your keyring, before you rely on it:
gpg --fingerprint E72439CEDD8511F9D607550B87FD60D5EB4A0B29
keys.openpgp.org for this keyIt is the default keyserver in several GnuPG builds, and it serves a stripped copy: the bare
public-key packet, with no user ID and no self-signature. GnuPG refuses that copy —
gpg: key 87FD60D5EB4A0B29: new key but contains no user ID - skipped — and the
key never enters the keyring, so the verify below fails with what looks like a bad signature and
is in fact a missing key. That server strips user IDs by policy until an address is confirmed
through it, which is not something a pseudonymous project can do.
gpg --verify SHA256SUMS.randomx.asc SHA256SUMS.randomx
Today this reports No such file or directory,
because no release has published that file yet. See the note at the top of this section.
gpg will add that the key is not certified with a trusted signature. That
is expected and it is not a failure. It means you have not told your keyring that you
personally vouch for this key, which is exactly what the fingerprint is there to settle. What
matters is that the signature verifies, and that the key it verified against is the fingerprint
printed above.
The whitepaper is signed too#
The PDF served from this site has a detached signature beside it, made by the same key:
curl -fsSLO https://zycord.com/assets/zycord-whitepaper.pdf
curl -fsSLO https://zycord.com/assets/zycord-whitepaper.pdf.asc
gpg --verify zycord-whitepaper.pdf.asc zycord-whitepaper.pdf
The rebuild — the check that actually replaces a certificate#
A hash tells you the file was not altered in transit. It says nothing about what the publisher put in it. This does:
git clone https://gitlab.com/zycord-group/zycord-node.git
cd zycord-node
git checkout v<version>
make build
sha256sum bin/zcd bin/zycordd
Compare against SHA256SUMS.binaries from the release. They must match
exactly. If they do, the binary you downloaded contains this source and nothing else
— a claim no certificate has ever made about anything.
The Go toolchain is part of what you are reproducing#
It is go1.26.2. The same source compiled by two different Go releases is two
different binaries; that is not a subtlety, it is measurable — this repository's own release
workflow once recorded three separate SHA-256 values for one commit and one set of flags, one per
Go version. So make build pins GOTOOLCHAIN=go1.26.2 and stops with an
explanation if it cannot use that toolchain, instead of producing a mismatch that looks like the
tampering this whole page is here to detect.
Two consequences worth having:
- You do not have to trust the version written above. The released binary states
it:
go version -m zcdprints the toolchain that built it, next to the module and the build flags. If that line and theMakefile's pin ever disagree, the binary was not built the way this page says. - Checking out an older tag checks out its pin along with its source, so rebuilding an old release keeps working after the project moves to a newer Go.
Line endings are part of the source here#
wallet/webui embeds the wallet frontend with //go:embed, so those
files' line endings are bytes in the binary: a checkout converted to CRLF builds a different
zcd from the same commit — a mismatch with an innocent cause and a frightening
appearance. A .gitattributes carrying * text=auto eol=lf gives LF in the
working tree on every platform and every git configuration, so a fresh clone
gives the same answer everywhere.
Two cases it does not cover: rebuilding a tag older than that file (set
core.autocrlf=input before checking out), and an existing clone made with
core.autocrlf=true, which does not heal on pull.
-randomx archives are not reproducible, by constructionRandomX is C++, so a cgo build carries a system C toolchain into the output and nobody can
rebuild it byte for byte. Those archives are checksummed by SHA256SUMS.randomx and
are deliberately absent from SHA256SUMS.binaries, which lists what a build you
make from source should hash to. This is the one place where the
tier that joins a network and the tier that is attested do not overlap — see the two-tiers
table under Installation.
Stating it is the point: it is your decision rather than an assumption made for you.
Check the implementation against the protocol#
Separately from checking a binary's provenance, you can check that it implements the protocol. The golden vectors are the protocol; an independent implementation that passes them is a peer, not a fork.
zcd vectors # check this build against spec/vectors
zcd genesis --testnet # rebuild block 0 and print its id
zcd params --testnet # print the frozen parameters
zcd genesis is the command that matters most: a launch announcement commits, weeks
in advance, to the code tag, the parameter hash and the genesis id — and this rebuilds all
of them in milliseconds, from source, on any machine. The public testnet's values are on the
testnet page; a build that prints different ones is a build on a
different chain, and it will not connect.
Independent attestations#
One person rebuilding a tag proves the tag is reproducible. Several strangers rebuilding it and signing the result proves rather more, and it is the pattern serious projects converged on for exactly this reason. Rebuild a tag, then post the digest you got and the tag you built in the announcement thread — including when it does not match, which is the case worth hearing about.