fix(release): embed the verified Wownero 0.11.5.0 daemon

This commit is contained in:
2026-10-17 05:59:54 -04:00
parent d9cd8e328e
commit 34bdecf5ac
9 changed files with 194 additions and 88 deletions
+4 -14
View File
@@ -53,21 +53,11 @@ jobs:
- name: Verify exact reviewed source
run: python3 -B contrib/release/check_dispatch.py
- name: fetch wownerod to embed
- name: fetch verified wownerod to embed
run: |
# ubuntu-latest and windows-latest both resolve to the same host in
# host mode, so a fixed /tmp path lets a concurrently dispatched job's
# `rm -rf` delete this job's binary between fetch and configure. Scope
# every extract to the run.
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
curl -fL -o "$EMBED/wownero.tar.bz2" \
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-linux-gnu-v0.11.4.0.tar.bz2
rm -rf "$EMBED/wow-x" && mkdir -p "$EMBED/wow-x"
tar xjf "$EMBED/wownero.tar.bz2" -C "$EMBED/wow-x"
find "$EMBED/wow-x" -name wownerod -type f -exec cp {} "$EMBED/wownerod-embed/" \;
chmod +x "$EMBED/wownerod-embed/wownerod"
ls -la "$EMBED/wownerod-embed"
python3 -B contrib/release/fetch_wownerod.py \
--target x86_64-linux-gnu \
--destination "/tmp/wowlet-${GITHUB_RUN_ID}/wownerod-embed"
- name: fetch tor to embed
run: |
+10 -15
View File
@@ -12,7 +12,7 @@ name: macos build (gitea self-hosted)
# APPLE_CERTIFICATE (b64 Developer ID Application .p12),
# APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID,
# APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_P8_BASE64 (notarytool).
# If APPLE_CERTIFICATE is unset the build ships unsigned instead of failing.
# Missing signing or notarization inputs refuse before compilation.
on:
workflow_dispatch:
@@ -59,22 +59,17 @@ jobs:
}
done
- name: fetch wownerod to embed
- name: fetch verified wownerod to embed
run: |
set -e
ARCH=$(uname -m)
case "$ARCH" in
arm64) WOWTAR=wownero-aarch64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
*) WOWTAR=wownero-x86_64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
case "$(uname -m)" in
arm64) target=aarch64-apple-darwin11 ;;
x86_64) target=x86_64-apple-darwin11 ;;
*) echo "Unsupported macOS build architecture" >&2; exit 1 ;;
esac
mkdir -p /tmp/wownerod-embed /tmp/wow-extract
curl -L -o /tmp/wownero.tar.bz2 \
"https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/$WOWTAR"
# /tmp is a symlink on macOS — find(1) won't descend it as a bare argument
tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-extract
find /tmp/wow-extract -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \;
chmod +x /tmp/wownerod-embed/wownerod
ls -la /tmp/wownerod-embed
EMBED="/tmp/wowlet-${GITHUB_RUN_ID}/wownerod-embed"
python3 -B contrib/release/fetch_wownerod.py --target "$target" --destination "$EMBED"
echo "WOWNEROD_DIR=$EMBED" >> "$GITHUB_ENV"
- name: fetch tor to embed
run: |
@@ -108,7 +103,7 @@ jobs:
export CPATH="$(brew --prefix)/include"
export LIBRARY_PATH="$(brew --prefix)/lib"
mkdir -p build && cd build
cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DBoost_USE_STATIC_LIBS=ON -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" ..
cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DBoost_USE_STATIC_LIBS=ON -DWOWNEROD_DIR="$WOWNEROD_DIR" -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" ..
cmake --build . -j "$(sysctl -n hw.ncpu)"
- name: package app bundle
+4 -12
View File
@@ -107,19 +107,11 @@ jobs:
path: contrib/depends/built
key: depends-built-mingw32-${{ hashFiles('contrib/depends/packages/*', 'contrib/depends/hosts/*', 'contrib/depends/toolchain.cmake.in') }}
- name: fetch wownerod.exe to embed
- name: fetch verified wownerod to embed
run: |
# This job and the Linux one land on the same host in host mode, and a
# fixed /tmp path let each wipe the other's binary mid-build. Scope the
# extract to the run.
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
curl -fL -o "$EMBED/wownero-win.zip" \
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-w64-mingw32-v0.11.4.0.zip
# -j flattens the versioned subdir so wownerod.exe is a DIRECT child
# (WownerodQrcGenerator globs ${WOWNEROD_DIR}/* non-recursively).
unzip -o -j "$EMBED/wownero-win.zip" 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d "$EMBED/wownerod-embed"
ls -la "$EMBED/wownerod-embed"
python3 -B contrib/release/fetch_wownerod.py \
--target x86_64-w64-mingw32 \
--destination "/tmp/wowlet-${GITHUB_RUN_ID}/wownerod-embed"
- name: build wowlet.exe
run: |
+40 -45
View File
@@ -1,46 +1,41 @@
# Release process
# Wowlet release process
- Before a minor release:
- Update `src/assets/restore_heights_monero_{mainnet,stagenet}.txt`
- To obtain values, run `contrib/generate-restore-heights/heights.py`
- Update default node lists in `src/assets/nodes.json`
- Bump `openssl`, `qt`, `tor_*` packages in `contrib/depends/packages`
- Update or patch any statically linked dependencies that have known vulnerabilities
- Run `feather-utils/depends/vulns.py` to check
- Rebase on top of latest Monero version
- Update `src/assets/ack.txt`
- Update documentation (`feather-wallet/feather-docs`) and `external/feather-docs` submodule
- Update the version number in `CMakeLists.txt`
- A hardfork-ready release must bump major version
- Create an annotated tag (`git tag x.x.x -a`)
- Tag must match version in `CMakeLists.txt`
- Only commits that update the version number in `CMakeLists.txt` may be tagged
- Push the master branch and tags
- `git push --tags origin master`
- Run `guix` builds in a clean repo:
- ```bash
git clone https://github.com/feather-wallet/feather.git
cd feather
git checkout <TAG>
git submodule update --init --recursive
./contrib/guix/guix-build
```
- Alternatively, run `feather-utils/guix/run-build.sh`
- Attest the builds (`contrib/guix/guix-attest`) and PR to `feather/feather-sigs`.
- Wait until at least one other person has independently verified reproducibility
- If builds are not reproducible: fix any reproducibility defects and bump patch version. Do not sign or release non-reproducible builds.
- Sign release artifacts and hashlists.
- Transfer files in `guix-build-x.x.x/output` to release signing machine
- Run `feather-utils/release/make-release.sh`
- Update the site (`feather-wallet/feather-site`)
- Run the `feather-utils/site/bump-version.py` script to create a template commit.
- Edit the changelog in `content/changelog`
- If a release includes security fixes add "(includes security fixes)" to the title.
- Update the version number, file sizes and paths in `data/release.json`
- Upload releases, signatures and signed hashlists.
- Follow the directory structure defined in `MainWindow::onShowUpdateCheck`.
- Make `depends` source files [available](https://featherwallet.org/files/sources/):
- `make -C contrib/depends download`
- Announce release on irc/Matrix
- Update websocket servers to notify clients of new release
- Wait up to 7 days to allow for bug reports before major rollout
GitHub `wownero/wowlet` owns source and public releases. Private `Builds/wowlet`
is the build ingress on our hardware. Fleet owns dispatch and publication plans.
A source merge, a build, a code signature and a published release are separate outcomes.
1. Prepare and review a GitHub pull request with the version, wallet-library pin,
support documentation and release inputs. Publish a new library commit to its
canonical repository before pinning it. Keep source and dependency revisions exact.
2. `make check` runs the cheap source checks. Run the private Linux, Windows and
macOS workflows through reviewed Fleet admission with `expected_sha` equal to
the protected Builds commit. Preserve canonical source and Builds commit identities
separately. Do not run release builds on GitHub.
3. The build helpers verify embedded Wownero archives against `contrib/release/wownerod.json`.
That file records the reviewed checksum provenance. Do not describe an unsigned
checksum list as a PGP verification.
4. Collect artifacts by their run and artifact identities. Require a Linux AppImage,
macOS archive, signed Windows archive and unsigned Windows archive. Check archive
paths, bundled runtimes and packaging requirements. Never select an artifact by size.
5. Require Developer ID signing, accepted notarization and stapling for macOS.
Require valid, timestamped Such Software LLC Authenticode signing for Windows.
The EV token stays on its signing host; CI never receives its password or key.
6. Record any manual wallet checks actually performed. Manual testing and independent
reproducibility are separate evidence; successful builds do not prove either.
The unsigned Windows archive is provided for reproduction, without claiming an
independent reproduction occurred.
7. Create `hashes.txt` over the four final archives. Sign it on the owner's signing
machine with the existing release key `9F9930300530DF724761B7055A881347310EB79F`.
Verify the detached signature and all hashes before publication. Never transfer
the private key or put a passphrase in a command, environment or log.
8. Create an annotated `vX.Y.Z` tag from the exact reviewed canonical source, matching
`CMakeLists.txt`. Use Fleet's reviewed publication plan to upload the four archives,
`hashes.txt` and `hashes.txt.asc` to GitHub Releases. Refuse replacement of existing
tags or assets; recover partial publication from retained intent.
9. Download the public assets again and verify their hashes and PGP signature. Record
the tag, source commit, Builds commit, run identities, signing evidence and public
URL in the private journal. Release notes should say when security fixes are included.
Additional architectures and Guix builds remain separate lanes. Do not claim a
platform is released merely because its workflow exists. Announcements are an
owner action, separate from publishing the release files.
+73
View File
@@ -0,0 +1,73 @@
"""Fetch the pinned daemon archive and copy only its verified daemon binary."""
import argparse
import hashlib
import json
from pathlib import Path, PurePosixPath
import shutil
import tarfile
import tempfile
import urllib.request
import zipfile
MAX_ARCHIVE = 300 * 1024 * 1024
MAX_BINARY = 128 * 1024 * 1024
TARGETS = {"x86_64-linux-gnu", "x86_64-w64-mingw32", "aarch64-apple-darwin11", "x86_64-apple-darwin11"}
def copy_daemon(archive, destination, windows):
name = "wownerod.exe" if windows else "wownerod"
if windows:
container = zipfile.ZipFile(archive)
members = [m for m in container.infolist() if PurePosixPath(m.filename).name == name]
valid = lambda m: not m.is_dir() and (m.external_attr >> 16 & 0o170000) in (0, 0o100000) and 0 < m.file_size <= MAX_BINARY
reader = container.open
else:
container = tarfile.open(archive, "r:bz2")
members = [m for m in container.getmembers() if PurePosixPath(m.name).name == name]
valid = lambda m: m.isfile() and 0 < m.size <= MAX_BINARY
reader = container.extractfile
with container:
if len(members) != 1 or not valid(members[0]):
raise ValueError("Pinned archive must contain exactly one regular daemon binary")
# Do not extract archive paths, symlinks or unrelated files.
with reader(members[0]) as source, (destination / name).open("xb") as output:
shutil.copyfileobj(source, output)
(destination / name).chmod(0o755)
def fetch(target, destination):
manifest = json.loads(Path(__file__).with_name("wownerod.json").read_text())
suffix = ".zip" if target.endswith("mingw32") else ".tar.bz2"
filename = f"wownero-{target}-{manifest['version']}{suffix}"
expected = manifest["archives"][filename]
destination.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="wowlet-daemon-") as staging:
archive = Path(staging) / filename
digest = hashlib.sha256()
size = 0
url = f"https://github.com/wownero/wownero/releases/download/{manifest['version']}/{filename}"
with urllib.request.urlopen(url, timeout=120) as response, archive.open("xb") as output:
if not response.geturl().startswith("https://"):
raise ValueError("Daemon download redirected outside HTTPS")
while chunk := response.read(1024 * 1024):
size += len(chunk)
if size > MAX_ARCHIVE:
raise ValueError("Daemon archive exceeds size bound")
digest.update(chunk)
output.write(chunk)
if digest.hexdigest() != expected:
raise ValueError("Daemon archive SHA-256 differs from the reviewed release pin")
copy_daemon(archive, destination, target.endswith("mingw32"))
print(f"Verified {filename}: {expected}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", required=True, choices=sorted(TARGETS))
parser.add_argument("--destination", required=True, type=Path)
args = parser.parse_args()
fetch(args.target, args.destination)
if __name__ == "__main__":
main()
@@ -0,0 +1,49 @@
import importlib.util
import io
from pathlib import Path
import tarfile
import tempfile
import unittest
from unittest.mock import patch
SPEC = importlib.util.spec_from_file_location("fetch_wownerod", Path(__file__).parents[1] / "fetch_wownerod.py")
fetcher = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(fetcher)
class DaemonTests(unittest.TestCase):
def test_checksum_failure_never_extracts_a_daemon(self):
response = io.BytesIO(b"untrusted archive bytes")
response.geturl = lambda: "https://release-assets.githubusercontent.com/archive"
with tempfile.TemporaryDirectory() as tmp, patch.object(fetcher.urllib.request, "urlopen", return_value=response), patch.object(fetcher, "copy_daemon") as extract:
with self.assertRaisesRegex(ValueError, "SHA-256"):
fetcher.fetch("x86_64-linux-gnu", Path(tmp) / "daemon")
extract.assert_not_called()
self.assertFalse((Path(tmp) / "daemon/wownerod").exists())
def test_archive_paths_cannot_write_outside_destination(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
archive = root / "daemon.tar.bz2"
with tarfile.open(archive, "w:bz2") as output:
info = tarfile.TarInfo("../../wownerod")
info.size = 3
output.addfile(info, io.BytesIO(b"exe"))
destination = root / "stage"
destination.mkdir()
fetcher.copy_daemon(archive, destination, False)
self.assertEqual((destination / "wownerod").read_bytes(), b"exe")
self.assertFalse((root / "wownerod").exists())
def test_symlink_daemon_is_refused(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
archive = root / "daemon.tar.bz2"
with tarfile.open(archive, "w:bz2") as output:
info = tarfile.TarInfo("wownerod")
info.type = tarfile.SYMTYPE
info.linkname = "/etc/passwd"
output.addfile(info)
with self.assertRaisesRegex(ValueError, "regular daemon"):
fetcher.copy_daemon(archive, root, False)
self.assertFalse((root / "wownerod").exists())
+12
View File
@@ -0,0 +1,12 @@
{
"version": "v0.11.5.0",
"source_commit": "b22382c55fa2a97cd70aca450026b8e2bb8711a6",
"checksum_evidence": "https://github.com/wownero/wownero/actions/runs/37402323531",
"checksum_signature": "The release tag build replaced the archives after SHA256SUMS.txt.asc was signed. These hashes match the successful tag build and current release metadata; that signature does not cover these archives.",
"archives": {
"wownero-x86_64-w64-mingw32-v0.11.5.0.zip": "46dffbd313de1df4667516acf23737f986eb2e131c06a8dde1d11dcff2eb06e2",
"wownero-aarch64-apple-darwin11-v0.11.5.0.tar.bz2": "bad8cc7f1c78d026e63dbe3ffc0f2175d50b986bbde3fc18826a04ff693866d4",
"wownero-x86_64-apple-darwin11-v0.11.5.0.tar.bz2": "ae562c90a10856fcdd183dc9530dff73e46ed6eece9fb8dcfaa77f1e07174787",
"wownero-x86_64-linux-gnu-v0.11.5.0.tar.bz2": "2d0cc62c24255d45c51936616559651378011501c55a8caadb9a125bb875849b"
}
}
+1 -1
View File
@@ -606,7 +606,7 @@ Rectangle {
horizontalAlignment: Text.AlignHCenter
color: "#f2dcb4"
font.pointSize: 9
text: "Mining needs your own node.\nYou are connected to a remote one — " +
text: "Mining needs your own node.\nYou are connected to a remote one. " +
"start the built-in node or point the wallet at a local daemon " +
"under Settings to mine."
}
+1 -1
View File
@@ -137,7 +137,7 @@
<item row="3" column="1">
<widget class="QLabel" name="label_11">
<property name="text">
<string>codeberg.org/wownero/wowlet</string>
<string>github.com/wownero/wowlet</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByMouse</set>