mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 23:31:32 +02:00
fix(release): embed the verified Wownero 0.11.5.0 daemon
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user