ci: add GHA test workflow + GPG-verify bitcoind/elementsd downloads (#2606)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
k9ert 2026-04-17 20:50:12 +02:00 committed by GitHub
parent 3e277689ef
commit a24df2eb50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1070 additions and 6 deletions

215
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,215 @@
name: Tests
on:
pull_request:
push:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-22.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
cache-dependency-path: requirements.txt
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev libudev-dev python3-virtualenv
- name: Cache bitcoind and elementsd
uses: actions/cache@v4
with:
path: |
./tests/bitcoin
./tests/bitcoin-*
./tests/elements
./tests/elements-*
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
save-always: true
- name: Install bitcoind
run: ./tests/install_noded.sh --debug --bitcoin binary
- name: Install elementsd
run: ./tests/install_noded.sh --debug --elements binary
- name: Install Python dependencies
run: |
pip install -r requirements.txt --require-hashes
pip install -e ".[test]"
- name: Fetch version tag
run: git fetch origin refs/tags/v1.0.0
- name: Run pytest
run: >
pytest
--cov=cryptoadvance
--cov-report=term
--junitxml=./testresults.xml
-p no:cacheprovider
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: testresults
path: ./testresults.xml
cypress:
runs-on: ubuntu-22.04
timeout-minutes: 30
container:
image: ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:149f19f9534a3932972151d9f0084b386f8beef1b897ff739ad64a962af891dc
options: --shm-size=2g
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fix virtualenv shim
shell: bash
run: |
echo -e '#!/bin/bash\npython3 -m virtualenv "$@"' > /usr/local/bin/virtualenv
chmod +x /usr/local/bin/virtualenv
virtualenv --version
- name: Cache bitcoind and elementsd
uses: actions/cache@v4
with:
path: |
./tests/bitcoin
./tests/bitcoin-*
./tests/elements
./tests/elements-*
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
save-always: true
- name: Install bitcoind
run: ./tests/install_noded.sh --debug --bitcoin binary
- name: Install elementsd
run: ./tests/install_noded.sh --debug --elements binary
- name: Install Python dependencies
shell: bash
run: |
# Container uid differs from checkout owner — trust the repo dir so
# setuptools-scm/vcs_versioning can run `git describe` during pip -e.
git config --global --add safe.directory "$GITHUB_WORKSPACE"
virtualenv --python=python .env
source ./.env/bin/activate
pip3 install -r requirements.txt --require-hashes
pip3 install -e ".[test]"
- name: Cache npm modules
uses: actions/cache@v4
with:
path: ./node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Install npm deps
run: npm ci
- name: Run Cypress
shell: bash
run: |
source ./.env/bin/activate
./utils/test-cypress.sh --debug run
- name: Upload Cypress test results
if: always()
uses: actions/upload-artifact@v4
with:
name: cypress-results
path: cypresstest-output.xml
- name: Upload Cypress screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: cypress-screenshots
path: cypress/screenshots/**
if-no-files-found: ignore
- name: Upload Cypress videos
if: always()
uses: actions/upload-artifact@v4
with:
name: cypress-videos
path: cypress/videos/**
if-no-files-found: ignore
extension-smoketest:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
cache-dependency-path: requirements.txt
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev libudev-dev python3-virtualenv
- name: Cache bitcoind and elementsd
uses: actions/cache@v4
with:
path: |
./tests/bitcoin
./tests/bitcoin-*
./tests/elements
./tests/elements-*
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
save-always: true
- name: Install bitcoind
run: ./tests/install_noded.sh --debug --bitcoin binary
- name: Install elementsd
run: ./tests/install_noded.sh --debug --elements binary
- name: Install Python dependencies
run: |
virtualenv --python=python .env
source ./.env/bin/activate
pip3 install -r requirements.txt --require-hashes
pip3 install -e ".[test]"
- name: Extension smoketest
run: |
git config --global user.name "CI CD"
git config --global user.email "cicd@example.com"
source ./.env/bin/activate
echo $PATH
mkdir tmp && cd tmp
mkdir testextension && cd testextension
pwd
python3 -m cryptoadvance.specter ext gen --ext-id cicdtest --org cryptoadvance --no-isolated-client --devicename cicddevice
pip3 install -e .
python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug 2> specter.log &
sleep 15
cat specter.log | grep "Found CicdtestService" # Discovery failed
cat specter.log | grep "Extension CicdtestService activated (alpha)"
sudo apt-get update && sudo apt-get -y install curl
curl http://127.0.0.1:25441/svc/cicdtest/ | grep "CicdtestService 4thewin."

2
.gitignore vendored
View file

@ -38,7 +38,9 @@ node_modules
btcd-conn.json
elmd-conn.json
tests/bitcoin*
!tests/bitcoin_SHA256SUMS
tests/elements*
!tests/elements_SHA256SUMS
token.sh
src/cryptoadvance/specter/translations/**/messages.mo
src/cryptoadvance/specter/_version.py

View file

@ -23,5 +23,9 @@
"mochaFile": "cypresstest-output.xml",
"toConsole": true
},
"experimentalSessionAndOrigin": true
"experimentalSessionAndOrigin": true,
"retries": {
"runMode": 1,
"openMode": 0
}
}

View file

@ -8,3 +8,5 @@ docker push ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411
```
Search for `cypress-python` on where this is used in the project.
When editing this Dockerfile, rebuild and push to `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy` with a new tag, then update the digest pin in `.github/workflows/test.yml`.

View file

@ -0,0 +1,561 @@
# Cirrus CI Replacement Spec
> Incorporates review findings from Winston (architecture), Amelia (impl),
> Murat (test arch), Mary (requirements), and Ravi (red team) across two
> party-mode rounds plus a security pass. Key decisions: GHCR image
> migration is already complete (landed in `kn/update-node-ci`); cutover
> split across two PRs; acceptance gate is flake-rate based with 30-day
> steady-state budget; branch-protection rename has a runbook; committed
> evidence artifact required at gate-close; Codecov is out of scope.
> **CRITICAL security regression identified and fixed as PR #1 blocker:**
> unverified bitcoind downloads plus `save-always` cache turn a
> supply-chain attack into a persistent backdoor vector — PR #1 must
> update `tests/install_noded.sh` to GPG-verify releases.
## Context
Cirrus CI's free OSS tier shuts down **end of June 2026**. Specter-Desktop
depends on Cirrus for PR gating via `.cirrus.yml` (pytest + Cypress +
extension smoketest). We must migrate before the shutdown or lose PR gating.
Today's CI split (see `AGENTS.md` §CI/CD):
| System | Purpose | Status |
|----------------|-----------------------------------------------------|-----------------|
| GitHub Actions | Lint, smoke-build, Docker, **full release pipeline** | Active |
| Cirrus CI | pytest + Cypress + extension smoketest on PRs | **Sunsetting** |
| GitLab CI | `check` job only; release flow already migrated off | Vestigial, dead |
**Reframe:** the real root cause is **3-system CI sprawl with no owner**.
Cirrus shutdown is the forcing function to consolidate on GH Actions.
## Goals
1. Preserve PR gating equivalent to Cirrus (pytest, Cypress, extension
smoketest) before Cirrus goes dark.
2. Zero new private-hardware requirements — stay on public runners.
3. Preserve cache hit rate so PR turnaround doesn't regress beyond SLO
(see §SLOs).
4. Leave the repo in a state where `.cirrus.yml`, `docker/cirrus-jammy/`,
`.gitlab-ci.yml`, and `pyinstaller/build-win-ci.bat` can be removed
cleanly in a follow-up PR.
5. Remove the cross-provider GitLab registry dependency entirely.
## Non-goals
- Re-architecting the test suite.
- Rebalancing the test pyramid (Cypress → pytest migration). Tracked
separately in §Deferred.
- Moving jobs back to GitLab CI (no PR model).
- Adding Cypress Dashboard or other paid observability SaaS.
- Coverage upload to Codecov or similar. Coverage stays terminal-only,
matching current Cirrus behavior.
- Introducing new test matrices (OS/python versions) beyond Cirrus parity.
## Target: GitHub Actions
GH Actions already hosts lint, Docker, and the release pipeline, all on
public runners. Consolidating reduces CI systems from three to one.
## Image hosting — already on GHCR
**Already done** by PR #2602 (merged 2026-04-12, commit `3e277689`). That
PR upgraded Node 12→18 and migrated **all three** CI images from GitLab
registry to GHCR:
- `ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412`
- `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411`
- `ghcr.io/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20260411`
The cross-provider GitLab registry dependency is gone. No mirror workflow
needed. `.cirrus.yml` already references the GHCR paths, so Cirrus and the
new GHA workflow will pull from the same registry during the side-by-side
period.
**Residual ownership gap (small):** the Dockerfiles are built by hand per
`docker/cypress-python-jammy/Readme.md`. When they change (historically
every ~18 months), whoever edits must remember to `docker build && docker push`
to GHCR and bump the tag in `test.yml`. Fix: add one sentence to
`docker/cypress-python-jammy/Readme.md` stating that Dockerfile edits
require re-push to GHCR and a matching tag/digest bump in
`.github/workflows/test.yml`. That's the full remediation — no scheduled
liveness check, no CODEOWNERS fight. The image is too static to justify more.
Deeper hardening (cosign signing, automated build workflow) is tracked in
§Security HIGH as a 30-day follow-up.
## What to port
Three Cirrus tasks → three GH Actions jobs in a new
`.github/workflows/test.yml`. Trigger: `pull_request` + `push` to master.
### Global workflow-level settings
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
```
Every job must set:
- `timeout-minutes:` (budget below)
- `actions/checkout@v4` with **`fetch-depth: 0`** (NOT default `1`) —
`tests/test_util_version.py` needs tag history and `git describe` needs
annotated tags. No separate `git fetch refs/tags/v1.0.0` dance.
- `fail-fast: false` semantics where matrices are used.
### 1. `test` (pytest)
Cirrus today: `test_task` on
`ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412`
(post-PR-#2602 baseline), runs
`pytest --cov=cryptoadvance --junitxml=./testresults.xml` with cached
bitcoind and elementsd.
GH Actions mapping:
- `runs-on: ubuntu-22.04`
- `timeout-minutes: 45`
- System deps installed inline: `libusb-1.0-0-dev libudev-dev
python3-virtualenv`. No custom image.
- `actions/setup-python@v5` pinned to `3.10`, `cache: pip`, `cache-dependency-path: requirements.txt`.
- **bitcoind/elementsd cache** (see §Caching).
- Install:
```
pip install -r requirements.txt --require-hashes
pip install -e ".[test]" # intentionally bypasses hashes, documented
```
- Run: `pytest --cov=cryptoadvance --cov-report=term --junitxml=./testresults.xml -p no:cacheprovider --reruns 0`
- `--reruns 0`: fail fast. Flakiness is debt, not a coping mechanism.
- Coverage is terminal-only, matching current Cirrus behavior. No upload.
- Artifacts: `testresults.xml`.
### 2. `cypress`
Cirrus today: `cypress_test_task` on
`ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411`
(post-PR-#2602 baseline), Cirrus-requested `cpu: 6, memory: 6G`,
runs `./utils/test-cypress.sh --debug run`.
GH Actions mapping:
- `runs-on: ubuntu-22.04` (4 vCPU / 16 GB free). **Do not pre-escalate** to
`ubuntu-22.04-large`. Measure first (see §Cypress measurement).
- `timeout-minutes: 30` initially; adjust after measurement.
- Container:
```yaml
container:
image: ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:<digest>
options: --shm-size=2g # Cypress OOMs on default 64M /dev/shm
```
Digest captured from the `:20260411` tag at PR #1 time. Pinned by digest,
not tag, to make future Dockerfile edits visibly require a workflow bump.
- Same bitcoind/elements cache as `test`.
- npm cache via `actions/cache` keyed on `package-lock.json` (not
`setup-node`; node already ships in the container).
- Cypress retries: `{ runMode: 1, openMode: 0 }` in `cypress.json` — one
retry max, logged loudly so flakes are visible, not hidden.
- Run: `./utils/test-cypress.sh --debug run`.
- Artifacts: `cypresstest-output.xml`, `cypress/screenshots/**`,
`cypress/videos/**`.
**Sharding decision:** deferred. If measured wall-clock > 15 min, split
specs across a 2-shard matrix. Don't split pre-emptively.
### 3. `extension-smoketest`
Straight port of Cirrus's `extension_smoketest_task`. Same runner/deps as
`test` job. Preserves the exact bash block: git identity, `ext gen`, server
boot, log-line grep, curl assertion. **Contract must stay byte-compatible**
— this job is the canary for downstream extension developers.
## Images
- **`docker/cirrus-jammy/`** — delete in PR #2. pytest job installs deps
inline on `ubuntu-22.04`; no custom image needed. The current GHCR tag
(`cirrus-jammy:20260412`) can stay in GHCR indefinitely as a harmless
artifact after the Dockerfile is deleted from the repo.
- **`cypress-python-jammy`** + **`cypress-base-ubuntu-jammy`** — already
on GHCR via PR #2602. Keep Dockerfiles in `docker/`. Add one sentence
to `docker/cypress-python-jammy/Readme.md`: *"When editing this
Dockerfile, rebuild and push to
`ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy` with a new
tag, then update the digest pin in `.github/workflows/test.yml`."*
## Caching
GH Actions `actions/cache@v4` keyed on:
```
${{ runner.os }}-${{ runner.arch }}-noded-
${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-
binary
```
`tests/bitcoin_gitrev_pinned` is not in the repo today (bitcoin version
comes from `pyproject.toml`); the cache key hashes the committed
SHA256SUMS trust anchors instead, so a version bump invalidates the
cache through the trust-anchor files.
`runner.arch` matters — future ARM runners must not poison x86 caches.
Paths: `./tests/bitcoin`, `./tests/elements`.
Populate step: `./tests/install_noded.sh --debug --bitcoin binary` and
`--elements binary` on cache miss (match Cirrus `populate_script`).
Set **`save-always: true`** on the cache step so a mid-job timeout on a
cold cache still persists the partial binaries. Otherwise first-PR-after-
pin-bump on every branch re-pays the full install cost.
**Security-critical:** see §Security. `save-always` + unverified
downloads was a persistent-backdoor vector. PR #1 must update
`install_noded.sh` to GPG-verify releases and re-verify on every cache
restore. This is non-negotiable.
First run after cutover repopulates per branch — one-off ~few-minute cost,
accepted.
## Concurrency, timeouts, retries
| Setting | Value |
|-----------------------|----------------------------------------------|
| Concurrency group | `${{ github.workflow }}-${{ github.ref }}` |
| Cancel in progress | `true` |
| `test` timeout | 45 min |
| `cypress` timeout | 30 min (tune after measurement) |
| `extension-smoketest` | 15 min |
| pytest reruns | **0** (fail fast) |
| Cypress `runMode` retries | **1** (log loudly) |
## Security
Red-team pass (Ravi, 2026-04-12) surfaced one **CRITICAL** regression
introduced by this migration, plus two HIGH findings tracked as follow-ups.
### CRITICAL — bitcoind/elementsd download verification (PR #1 blocker)
**Attack chain:** `tests/install_noded.sh:244` does a bare
`wget https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}`
with **no GPG signature verification and no SHA256SUMS check** (same for
Elements at line 241). Under Cirrus this was per-run ephemeral. Under GH
Actions with `actions/cache@v4 save-always: true` — and a cache key that
only hashes the pinned-rev files and the install script, **not** the
binary content — a single poisoned fetch (MITM, DNS/BGP hijack, upstream
compromise) writes a trojaned `bitcoind` into the cache. Every
subsequent run on master and PR branches restores it from cache. The
test runner executes it with the repo checkout mounted.
**Impact:** persistent backdoor across all PR and master runs until the
cache key rotates. Blast radius includes the release pipeline
(`release.yml`), which shares the repo's `GITHUB_TOKEN` boundary and
runs on the same runner pool.
**Regression status:** the `install_noded.sh` bare-wget predates this
migration. Cirrus's per-run ephemerality masked the weakness. Moving to
GHA with `save-always` caching **materially worsens** it from transient
to persistent. We do not inherit the risk silently — we fix it in PR #1.
**Fix (must land in PR #1):**
1. Update `tests/install_noded.sh` to download `SHA256SUMS` and
`SHA256SUMS.asc` alongside the binary tarball.
2. Import the Bitcoin Core release signing keys (fanquake, achow101)
into a temporary `GNUPGHOME` and verify `SHA256SUMS.asc`.
3. Verify the tarball hash matches the entry in `SHA256SUMS`. Abort on
mismatch.
4. Same treatment for ElementsProject downloads (`install_noded.sh:241`)
using Elements release signing keys.
5. Run the verification step **on cache hit as well** (not only cache
miss), so a tampered cache entry fails closed instead of being
trusted on restore.
6. Commit the expected `SHA256SUMS` content (or its content hash) to the
repo so post-restore verification has a trusted reference. Bump it
alongside `tests/bitcoin_gitrev_pinned` / `tests/elements_gitrev_pinned`.
### HIGH — Cypress image integrity chain (30-day follow-up)
**Attack chain:** `cypress-python-jammy` is built and pushed by hand.
Maintainer-laptop compromise at push time → attacker uploads a trojaned
image under legitimate GHCR credentials. Next Dockerfile edit, a
maintainer computes the digest from the compromised local build and
pins it in `test.yml`. Cypress runs as root inside, with the repo
checkout mounted.
**Not a PR #1 blocker.** Image has been untouched for 18 months; edit
frequency bounds exposure. Tracked as a separate hardening issue.
**Fix (separate PR, target: within 30 days of cutover):**
1. Automate image build in a workflow triggered on `docker/**` path
filter, using default `GITHUB_TOKEN` with `packages: write`.
2. Cosign keyless-sign the pushed image via GitHub OIDC.
3. `test.yml` verifies the signature via `cosign verify` before
consuming the container. Removes maintainer laptops from the trust
path entirely.
### HIGH (acknowledged) — Release pipeline shares token boundary
`.github/workflows/release.yml` ships specterd, pip package, and
Electron artifacts to real users. It lives in the same repo as
`test.yml` and shares the same `GITHUB_TOKEN` boundary and runner pool.
Any RCE in a test workflow runs on a runner that can read `release.yml`.
This migration does not change that exposure — but **fixing CRITICAL #1
above also de-risks the release pipeline**, since the same runner pool
consumes the same (now verified) binaries.
No spec change beyond this acknowledgment. Full release pipeline
hardening is out of scope.
### LOW findings (accepted / backlog)
- **Branch protection cutover gap:** minutes-wide, runbook adequate,
accept.
- **`GITHUB_TOKEN` on fork PRs:** default read-only, no
`pull_request_target`, no third-party actions, no secrets exposed.
Clean.
- **First-party actions pinned to major (`@v4`/`@v5`) not SHA:** GitHub
org compromise is a tier-1 ecosystem event. Defense-in-depth backlog
item, not blocking.
## Test quality — flake detection, quarantine, steady-state
Not in Cirrus today. Adding now, at minimum viable.
- **Flake signal:** parse JUnit XML for retry markers after each run; when
a Cypress spec retries-to-green, emit a GH Actions warning annotation
and append a line to a tracking gist (`flake-log.md`). No dashboard yet.
- **Quarantine policy:** any spec flagged flaky twice in 14 days gets
`@skip(reason="flaky", issue="#NNNN")` with a linked GH issue and a
**2-week SLA** to fix-or-delete.
- **Retry budget:** Cypress `runMode: 1`. Anything needing more is quarantined.
- **Steady-state flake SLO (post-cutover):** rolling 30-day rerun rate
must stay **≤ 1%** across all jobs. Same JUnit parser feeds a daily cron
job that computes the window and auto-opens a P2 issue on breach. This
turns flake detection from a one-shot cutover gate into a living quality
signal — without it we'll be right back here in six months.
## Cypress performance — measurement protocol
**Before** approving any runner upgrade:
1. Run the Cypress suite 5× on `ubuntu-22.04` free tier.
2. Record wall-clock p50 / p95 via `/usr/bin/time -v` wrapping the script.
3. Compare to last 5 Cirrus runs' wall-clock.
4. Ship it if p95 is within **Cirrus +20%**.
5. Only if p95 exceeds +20% or hits the 30-min timeout, evaluate in order:
(a) `--shm-size` bump, (b) spec sharding across 2 jobs,
(c) `ubuntu-22.04-large` (paid — requires approval).
## Migration plan — two PRs
### PR #1 — Add GH Actions workflow (side-by-side)
1. **Security blocker:** update `tests/install_noded.sh` per §Security
CRITICAL fix (GPG-verify `SHA256SUMS.asc`, verify tarball hash,
re-verify on cache restore). Commit trusted reference hashes
alongside `tests/bitcoin_gitrev_pinned` /
`tests/elements_gitrev_pinned`. This must land in the same PR as the
workflow.
2. Capture the `cypress-python-jammy` image digest from the `:20260411`
GHCR tag. Write it into `test.yml`.
3. Add `.github/workflows/test.yml` with all three jobs.
4. Add the one-sentence rebuild-and-push note to
`docker/cypress-python-jammy/Readme.md`.
5. **Do not touch** `.cirrus.yml`, `.gitlab-ci.yml`, or branch protection.
6. Land the PR. Both CI systems now run on every PR.
7. Iterate on the workflow until the acceptance gate (§Acceptance) passes.
8. Commit the evidence artifact to `docs/ci-migration-evidence.md` once
the gate is met.
### PR #2 — Cutover + cleanup
Only merge when PR #1 meets the acceptance gate.
1. Delete `.cirrus.yml` and `docker/cirrus-jammy/`.
2. Delete `.gitlab-ci.yml` and `pyinstaller/build-win-ci.bat`.
3. Audit and prune dead GitLab-only code paths in `utils/release.sh`,
`utils/release_helper.py`, `utils/github.py`.
4. Drop Cirrus + GitLab sections from `docs/continuous-integration.md`.
5. Update `AGENTS.md` CI/CD section.
6. **Rename required status checks** in GitHub branch protection (see
§Branch protection). This is a separate manual step, documented in the
PR body.
Splitting the cleanup out of PR #1 preserves fast revertability: if
anything melts in week 1 of side-by-side, PR #1 is trivially revertable
because it touches only additive files.
## Branch protection & required checks
**Scariest silent-failure mode.** If the required-check name on master
branch protection still says `Cirrus CI / test_task` after cutover,
*nothing is gating PRs anymore* and no alert fires.
Runbook (must execute as part of PR #2 merge):
1. Before merging PR #2: list current required checks via
`gh api repos/cryptoadvance/specter-desktop/branches/master/protection`.
2. Record the Cirrus check names.
3. Merge PR #2.
4. Immediately update branch protection: remove the Cirrus check names,
then add the exact GitHub Actions required-check contexts in the form
`<workflow name> / <job name>` as reported by the GitHub UI or API.
For this migration the workflow `name:` is `Tests` (see
`.github/workflows/test.yml`), so the three contexts to require are:
- `Tests / test`
- `Tests / cypress`
- `Tests / extension-smoketest`
Do NOT add bare `test` / `cypress` / `extension-smoketest` — those are
job ids, not emitted check contexts, and will silently fail to gate.
5. Open a throwaway test PR and verify those exact three contexts are
marked required and are actually gating merge.
6. Only then announce cutover complete.
## Secrets inventory
No new secrets required. The workflow only uses the default
auto-provided `GITHUB_TOKEN` (for `ghcr.io` push during the one-shot image
mirror and for any standard action plumbing).
Historical GitLab secrets (`GH_BIN_UPLOAD_PW`, `TWINE_PASSWORD`,
`GPG_PASSPHRASE`, `SSH_SPECTEREXT_DEPLOY_KEY`, `SSH_SPECTERSTATIC_DEPLOY_KEY`)
are already unused per AGENTS.md. PR #2 should not touch them (release
pipeline lives elsewhere).
## Stakeholder comms
This is CI plumbing, not a release. Scope is narrow:
- **Extension developers:** smoketest contract preserved byte-for-byte.
No comms needed unless the job fails post-cutover.
- **`docs/continuous-integration.md`:** updated in PR #2 to reflect the
new CI topology. That's the extent of external-facing docs.
## SLOs / acceptance criteria
### PR wall-clock SLO
**Baseline captured 2026-04-12** from the Cirrus GraphQL API
(`https://api.cirrus-ci.com/graphql`), last 20 successful master-branch
builds. Cirrus runs all three tasks in parallel, so total build wall-clock
≈ longest task (cypress).
| Task | n | median | p95 | min | max | GHA ceiling (+20%) |
|---------------------------|----|--------|--------|--------|--------|--------------------|
| `test_task` | 20 | 4m47s | 5m35s | 4m23s | 5m37s | **5m44s** |
| `cypress_test_task` | 20 | 6m02s | 6m55s | 5m27s | 8m15s | **7m14s** |
| `extension_smoketest_task`| 20 | 2m10s | 2m27s | 1m55s | 2m39s | **2m36s** |
- **Tolerance:** per-job wall-clock on GH Actions must not exceed the
"GHA ceiling" column (Cirrus median + 20%). This threshold is used
uniformly by the Cypress measurement protocol and the acceptance gate
— one number, one decision.
- **Action on breach:** investigate before cutover; do not ship PR #2 until
resolved.
**Implications for Cypress measurement protocol:** current Cirrus cypress
p95 is 6m55s — well under the 30-min timeout and not remotely at risk of
hitting paid-runner territory. The "4 vCPU vs 6" concern is almost
certainly a non-issue in practice. Measurement protocol stands, but expect
it to pass on free runners.
### Flake rate ceiling
- **Baseline:** measured during side-by-side (unknown today).
- **Ceiling:** post-cutover flake rate ≤ Cirrus baseline. Any regression
is a cutover blocker.
### Cost ceiling
- GH Actions minutes on public OSS runners are effectively free. No hard
cap needed unless we escalate to `-large` runners (then: $X/month ceiling
requires explicit approval from project lead).
### Acceptance gate for cutover (PR #2 merge criteria)
All four must hold:
1. **10 consecutive green runs** of the new workflow on master-branch
schedule (nightly) or via manual dispatch.
2. **At least 3 green PR runs**, including one that touches `src/cryptoadvance/specter/static/` or frontend templates.
3. **Zero new flakes** detected over at least 50 total runs (PR + master).
4. Cypress p95 wall-clock within **Cirrus +20%** per measurement protocol.
"Green on 3 PRs" alone is insufficient — flake rate is the real KPI.
### Evidence artifact (mandatory)
Before merging PR #2, a committed **evidence artifact** must exist at
`docs/ci-migration-evidence.md` containing, at minimum:
- Measured wall-clock median + p95 per job over the 10+ master runs
- Flake count over the 50+ runs window (expected: 0)
- URLs to 3+ sample PR runs including the frontend-touching one
- Date and commit SHA at measurement time
This exists so that six months from now "did we actually hit SLO?" has a
grep-able answer instead of a hope. GH Actions logs are GC'd; the evidence
doc is not. Ten minutes to write, permanent value.
## Rollback procedure
If anything breaks post-cutover:
1. **Workflow-level breakage in PR #1 phase:** disable the GH Actions
workflow via `workflow_dispatch` off-switch or delete the file; Cirrus
still gates PRs. Zero user impact.
2. **Post-cutover breakage (PR #2 merged):** revert PR #2. This restores
`.cirrus.yml` and `docker/cirrus-jammy/`. Re-add Cirrus checks to branch
protection. Cirrus is assumed still alive up to shutdown date.
3. **Post-Cirrus-shutdown breakage:** no Cirrus fallback exists. Forward
fix only. This is why PR #2 must merge **≥ 4 weeks before shutdown**.
## Timeline
| Date | Milestone |
|-------------|-------------------------------------------------|
| 2026-04-12 | Cirrus baseline captured (§SLOs); GHCR migration merged (PR #2602) |
| 2026-04-13 | Spec finalized post red-team pass |
| 2026-04-14 | Open PR #1 (security fix + workflow + cache) |
| 2026-04-21 | PR #1 green on first PR run |
| 2026-05-05 | Acceptance gate met (10 green + flake-clean) |
| 2026-05-12 | **PR #2 merged + branch protection updated** |
| 2026-05-19 | One full release cycle on new CI (if a tag cuts)|
| 2026-06-30 | Cirrus shutdown (~7 week buffer after cutover) |
## Deferred (explicit non-goals for this migration)
- **Test pyramid rebalance.** Cypress likely owns flows that belong in
pytest + a Flask test client. Q3 epic, not this PR. Logged here so it's
not lost.
- **Cypress Dashboard / paid observability.** Vendor lock, not worth the
cost at current scale. JUnit + artifacts cover 90% of the value.
- **Coverage upload / Codecov / coverage-delta gating.** Out of scope for
this migration. Matches current Cirrus (which also doesn't upload). If
we want trend data later, it's a standalone follow-up PR with its own
bus-factor discussion and token plumbing.
- **Local-contributor reproducibility of the Cypress image.** Nice to
have; separate runbook, post-cutover.
- **Cypress image cosign signing + build automation.** Tracked in
§Security HIGH. Separate PR within 30 days of cutover, not blocking.
- **Release pipeline hardening.** Out of scope; §Security HIGH
acknowledges shared token boundary.
- **First-party action SHA pinning.** Defense-in-depth backlog.
## Risks (updated)
1. **Supply-chain backdoor via unverified bitcoind (CRITICAL).** Resolved
by the PR #1 blocker fix in §Security. Without that fix, this spec is
unmergeable.
2. **Cypress perf on 4 vCPU.** Largely resolved by baseline data: Cirrus
cypress p95 is 6m55s, so the suite is not heavyweight. Residual risk is
that GH Actions 4-vCPU runners could push it past the +25% ceiling
(7m32s). Mitigation: measurement protocol in §Cypress measurement.
Escape hatch documented, not pre-purchased.
2. **Branch protection silent-ungate.** Scariest risk; has a runbook now
(§Branch protection).
3. **ghcr.io image rebuild drift.** Once mirrored, whoever edits
`docker/cypress-python-jammy/Dockerfile` must remember to rebuild and
re-push. Mitigation: README runbook + digest pinning makes staleness
visible (workflow won't auto-upgrade).
4. **First-run cache miss storm.** One-off few-minute cost. Accepted.
## Unresolved questions
None. All prior questions resolved or descoped.

35
tests/bitcoin_SHA256SUMS Normal file
View file

@ -0,0 +1,35 @@
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Provenance:
# Source URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Signature URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS.asc
# Verified on: 2026-04-15
# Verified by: Good signatures from:
# - fanquake (primary fingerprint E777299FC265DD04793070EB944D35F9AC3DB76A,
# signing subkey CFB16E21C950F67FA95E558F2EEB9F5CC09526C1)
# - achow101 (primary fingerprint 152812300785C96444D3334D17565732E08E5E41)
# Builder keys sourced from: https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
# Do not edit the hash lines below — they are the on-disk trust anchor compared
# against downloaded tarballs by tests/install_noded.sh::verify_binary.
9547fa03574f8bde296f707c7d9f7d89827c75c5a28f84402578a4fa92a787ec bitcoin-22.0-aarch64-linux-gnu-debug.tar.gz
ac718fed08570a81b3587587872ad85a25173afa5f9fbbd0c03ba4d1714cfa3e bitcoin-22.0-aarch64-linux-gnu.tar.gz
80071e0ecd24edfec8a1972b495b9822c79a5d33c7123bff51688638aac97cab bitcoin-22.0-arm-linux-gnueabihf-debug.tar.gz
b8713c6c5f03f5258b54e9f436e2ed6d85449aa24c2c9972f91963d413e86311 bitcoin-22.0-arm-linux-gnueabihf.tar.gz
8f70852feb39078e02182563517d17bdfc4a12904cf1bdabbae95594d9a1e473 bitcoin-22.0-codesignatures-22.0.tar.gz
d0e9d089b57048b1555efa7cd5a63a7ed042482045f6f33402b1df425bf9613b bitcoin-22.0.tar.gz
bfc04a3c4e8b613bfd9359e54da6cc60f027860e9723f9a6bfd6f13873eb811f bitcoin-22.0-powerpc64-linux-gnu-debug.tar.gz
2cca5f99007d060aca9d8c7cbd035dfe2f040dd8200b210ce32cdf858479f70d bitcoin-22.0-powerpc64-linux-gnu.tar.gz
5f0bf1491bc8825ca1506f7cf586030f06bb17a563ccde92e8c75720022704e6 bitcoin-22.0-powerpc64le-linux-gnu-debug.tar.gz
91b1e012975c5a363b5b5fcc81b5b7495e86ff703ec8262d4b9afcfec633c30d bitcoin-22.0-powerpc64le-linux-gnu.tar.gz
59b16e63aa935f50fd2813efe7f137187fcf0fff84e3205a9c6cb462a8bb160c bitcoin-22.0-riscv64-linux-gnu-debug.tar.gz
9cc3a62c469fe57e11485fdd32c916f10ce7a2899299855a2e479256ff49ff3c bitcoin-22.0-riscv64-linux-gnu.tar.gz
3b3e2680f7d9304c13bfebaf6445ada40d72324b4b3e0a07de9db807389a6c5b bitcoin-22.0-osx-signed.dmg
52449aa894a6ce5653315e1260d0ce87c1d9f490afe3c92b44285710804b11ae bitcoin-22.0-osx-unsigned.dmg
f51156774c24c0ac5cc30237fa08aa17ed04a180dfd72c3e7d20fdc3f45806dc bitcoin-22.0-osx-unsigned.tar.gz
2744d199c3343b2d94faffdfb2c94d75a630ba27301a70e47b0ad30a7e0155e9 bitcoin-22.0-osx64.tar.gz
3a4f05657c048d3e9505bdb9c4fb3658e5e3d4233b0b93c1853e080620589765 bitcoin-22.0-x86_64-linux-gnu-debug.tar.gz
59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16 bitcoin-22.0-x86_64-linux-gnu.tar.gz
9169989d649937c0f9ebccd3ab088501328aa319fe9e91fc7ea8e8cf0fcccede bitcoin-22.0-win64-setup.exe
f890473d6d910d478f8ff08f9356d0305d19b46cf06e4fc3b5a49b0b684fd2a7 bitcoin-22.0-win-unsigned.tar.gz
0a97ebc8ae44913e3ef9c5b1ddd2af3a4ffb0ba25b6ab1ee8173e40e60499402 bitcoin-22.0-win64-debug.zip
ecc579d006230d6ffc5a5b7b53ce8c76477d37c1c7bad69694e9c2d69f00331d bitcoin-22.0-win64-setup-unsigned.exe
9485e4b52ed6cebfe474ab4d7d0c1be6d0bb879ba7246a8239326b2230a77eb1 bitcoin-22.0-win64.zip

22
tests/elements_SHA256SUMS Normal file
View file

@ -0,0 +1,22 @@
# Trusted reference: plaintext hashes extracted from the clearsigned
# SHA256SUMS.asc published with elements-0.21.0.2.
# Provenance:
# Source URL: https://github.com/ElementsProject/elements/releases/download/elements-0.21.0.2/SHA256SUMS.asc
# Clearsigned: yes (PGP-SIGNED-MESSAGE, Hash: SHA256)
# Verified on: 2026-04-15
# Verified by: Good signature from Steven Roose <steven@stevenroose.org>
# primary fingerprint 8CC974D9CFD034DCEED213B02A57E0A610D7F19C
# signing subkey DE10E82629A8CAD55B700B972F2A88D7F8D68E87
# Key sourced from: hkps://keyserver.ubuntu.com (search 0x2F2A88D7F8D68E87)
# Do not edit the hash lines below — they are the on-disk trust anchor compared
# against downloaded tarballs by tests/install_noded.sh::verify_binary.
06de6cd39f00a8b5604f2baa6203c08358762b99560bec6ad90a65bc22fadf6f elements-elements-0.21.0.2-aarch64-linux-gnu.tar.gz
d263ed2182f55d0d08013f005f8db3d8d16c5f2a8e954b93f7938ceadd3798e0 elements-elements-0.21.0.2-arm-linux-gnueabihf.tar.gz
bbeac447c8947cb6788cc5d3f8e8eade82d9d03b159992fa22f4a0198ebd8a60 elements-elements-0.21.0.2-osx64.tar.gz
f7774aba06a4b4a3708be96774cd9e993fc58998f91a1a8cf9b692a9f888f89f elements-elements-0.21.0.2-osx-unsigned.dmg
9cdc53b1f81a8c2f3e4b04d80de04ca1cc182d018c0c4fef1aaaf6dcc87f5b42 elements-elements-0.21.0.2-osx-unsigned.tar.gz
cf3e2d6fc0811a3dd1e1ace640a62319af35443c76a404caf4f26674197c8059 elements-elements-0.21.0.2-riscv64-linux-gnu.tar.gz
a3ec010bd10e8b5947030e6b846069084056418a7a1e111c0d2634a65662e53e elements-elements-0.21.0.2-win64-setup-unsigned.exe
7a8f427be448eaa89f87c31cce705069c2d9528cf54fdd838c104f1257f800d4 elements-elements-0.21.0.2-win64.zip
4863ee842d13bbb0255c2002ccc07f170b87717589a3493805944579a4af6745 elements-elements-0.21.0.2-win-unsigned.tar.gz
3018116794429b77ce0dd7436c2906f8be4eb5d6163b8451c5ce7e7bedad152b elements-elements-0.21.0.2-x86_64-linux-gnu.tar.gz

View file

@ -221,6 +221,184 @@ function sub_compile {
echo " --> install_node.sh End $(date) took $DIFF"
}
# -----------------------------------------------------------------------------
# Release signing trust anchors for GPG defense-in-depth verification.
#
# These fingerprints (and the SHA256 trust anchors committed in
# tests/bitcoin_SHA256SUMS / tests/elements_SHA256SUMS) are the reason this
# script exists in its hardened form: CI uses `save-always: true` caches, so
# a single poisoned fetch would persist across every future run if the
# verification path were ever skipped. verify_binary() MUST be invoked on
# both cache miss (fresh download) AND cache hit (restore), and MUST abort
# non-zero on any mismatch.
#
# Builder keys for bitcoin-core releases are published at:
# https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
# The elements signing key was fetched from:
# hkps://keyserver.ubuntu.com (search 0x2F2A88D7F8D68E87)
# -----------------------------------------------------------------------------
# Pinned release signing keys (fingerprints). The committed SHA256SUMS files
# in tests/ are the primary trust anchor; the GPG step below re-validates the
# upstream chain when the network is reachable.
BITCOIN_RELEASE_KEYS=(
"E777299FC265DD04793070EB944D35F9AC3DB76A" # fanquake <fanquake@gmail.com>
"152812300785C96444D3334D17565732E08E5E41" # achow101 / Ava Chow
)
ELEMENTS_RELEASE_KEYS=(
"8CC974D9CFD034DCEED213B02A57E0A610D7F19C" # Steven Roose <steven@stevenroose.org>
)
# Compute the sha256 of a file in a cross-platform way.
# Usage: sha256_of <file>
function sha256_of {
local f=$1
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$f" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$f" | awk '{print $1}'
else
echo "ERROR: neither sha256sum nor shasum found" >&2
return 127
fi
}
# Look up the expected sha256 for ${basename_of_artifact} from the committed
# trust anchor file tests/${node_impl}_SHA256SUMS. Comment lines (starting
# with '#') in the trust anchor are ignored so the provenance preamble does
# not interfere.
# Usage: expected_sha_for <node_impl> <artifact_basename>
function expected_sha_for {
local node_impl=$1
local artifact=$2
local trust_file=""
# tests/install_noded.sh cd's to its own dir at the top of the file, so
# the trust anchors live one directory up from the usual CWD
# (./${node_impl}) and sometimes in the CWD itself, depending on whether
# verify_binary is called before or after we cd into ./${node_impl}.
if [ -f "./${node_impl}_SHA256SUMS" ]; then
trust_file="./${node_impl}_SHA256SUMS"
elif [ -f "../${node_impl}_SHA256SUMS" ]; then
trust_file="../${node_impl}_SHA256SUMS"
else
echo "ERROR: trust anchor tests/${node_impl}_SHA256SUMS not found" >&2
return 2
fi
# Format: <hex> <filename>
grep -v '^[[:space:]]*#' "$trust_file" \
| awk -v f="$artifact" '$2 == f {print $1; exit}'
}
# Import release signing keys into a throw-away GNUPGHOME and GPG-verify
# ${sumsfile} against ${sigfile}. Returns 0 on good signature, non-zero on
# anything else (including no network, missing gpg, bad sig). Callers MUST
# still compare ${sumsfile} to the committed trust anchor — this GPG step is
# defense in depth, not the sole trust anchor.
# Usage: gpg_verify_sums <node_impl> <sumsfile> <sigfile>
function gpg_verify_sums {
local node_impl=$1
local sumsfile=$2
local sigfile=$3
if ! command -v gpg >/dev/null 2>&1; then
echo " --> WARNING: gpg not installed; skipping upstream GPG re-verify (committed SHA256SUMS trust anchor still enforced)"
return 0
fi
local tmp_gnupg
tmp_gnupg=$(mktemp -d -t specter-gnupg-XXXXXX) || return 1
# shellcheck disable=SC2064
trap "rm -rf '$tmp_gnupg'" RETURN
export GNUPGHOME="$tmp_gnupg"
chmod 700 "$tmp_gnupg"
local keys=()
if [ "$node_impl" = "bitcoin" ]; then
keys=("${BITCOIN_RELEASE_KEYS[@]}")
elif [ "$node_impl" = "elements" ]; then
keys=("${ELEMENTS_RELEASE_KEYS[@]}")
fi
local imported=0
for fpr in "${keys[@]}"; do
# Try keys.openpgp.org first, then keyserver.ubuntu.com.
if curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/${fpr}" 2>/dev/null \
| gpg --import 2>/dev/null; then
imported=$((imported + 1))
continue
fi
if curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${fpr}&options=mr" 2>/dev/null \
| gpg --import 2>/dev/null; then
imported=$((imported + 1))
fi
done
if [ "$imported" -eq 0 ]; then
echo " --> WARNING: could not fetch any ${node_impl} release signing keys; skipping upstream GPG re-verify"
unset GNUPGHOME
return 0
fi
# Bitcoin Core SHA256SUMS.asc carries signatures from many maintainers.
# gpg --verify exits non-zero when ANY signature's key is missing, even
# if others verify successfully. We only import a subset of keys, so
# the raw exit code is unreliable. Instead, use --status-fd to check
# for at least one VALIDSIG whose primary-key fingerprint we trust.
#
# VALIDSIG format (last field is always the primary key fingerprint,
# even when the signature was made by a subkey):
# VALIDSIG <sign_fpr> <date> <ts> ... <primary_fpr>
# GOODSIG uses the signing subkey ID, which may differ from the primary
# fingerprint we pin — so VALIDSIG is the correct field to match.
local status_out
if [ -n "$sigfile" ] && [ -f "$sigfile" ]; then
status_out=$(gpg --status-fd 1 --verify "$sigfile" "$sumsfile" 2>/dev/null)
else
# Clearsigned (elements case): verify in-place.
status_out=$(gpg --status-fd 1 --verify "$sumsfile" 2>/dev/null)
fi
local found_good=0
for fpr in "${keys[@]}"; do
# Match VALIDSIG line whose last field (primary fingerprint) equals ours.
if echo "$status_out" | grep -q "VALIDSIG.*${fpr}"; then
found_good=1
echo " --> GPG: valid signature traced to primary key ${fpr}"
break
fi
done
unset GNUPGHOME
if [ "$found_good" -eq 0 ]; then
echo " --> WARNING: no VALIDSIG from a trusted primary key found in GPG status output"
echo " --> Status output: $status_out"
return 1
fi
return 0
}
# Verify a binary artifact (tarball) against the committed trust anchor.
# Aborts the script with exit 2 on any mismatch.
# Usage: verify_binary <node_impl> <path_to_binary_file>
function verify_binary {
local node_impl=$1
local binary_path=$2
if [[ ! -f "$binary_path" ]]; then
echo "ERROR: verify_binary: $binary_path does not exist" >&2
exit 2
fi
local artifact
artifact=$(basename "$binary_path")
local expected
expected=$(expected_sha_for "$node_impl" "$artifact")
if [ -z "$expected" ]; then
echo "ERROR: verify_binary: no trusted sha256 entry for '$artifact' in tests/${node_impl}_SHA256SUMS" >&2
exit 2
fi
local actual
actual=$(sha256_of "$binary_path") || exit 2
if [ "$expected" != "$actual" ]; then
echo "ERROR: verify_binary: sha256 mismatch for $artifact" >&2
echo " expected $expected" >&2
echo " actual $actual" >&2
echo " This is a trust violation. Aborting." >&2
exit 2
fi
echo " --> verify_binary: OK ($artifact sha256=$expected)"
}
function sub_binary {
node_impl=$1
echo " --> install_noded.sh Start $(date) (binary) for node_impl $node_impl"
@ -236,16 +414,52 @@ function sub_binary {
else
binary_file=${node_impl}-${version}-x86_64-linux-gnu.tar.gz
fi
if [[ ! -f $binary_file ]]; then
# The tarball lives inside ./${node_impl}/ so that it is captured by the
# cache (which covers ./tests/${node_impl}/) and re-verified on every
# cache restore. This is the whole point of the hardened flow — see the
# "save-always backdoor" rationale at the top of the file.
# Clean up a broken symlink from a partial cache restore (the workflow
# caches ./tests/${node_impl}-* as well, but be defensive).
if [[ -L "./${node_impl}" && ! -e "./${node_impl}" ]]; then
echo " --> cleaning up dangling symlink ./${node_impl}"
rm -f "./${node_impl}"
fi
mkdir -p "./${node_impl}"
local cached_tarball="./${node_impl}/${binary_file}"
if [[ -f "$cached_tarball" ]]; then
echo " --> cache hit: found existing ${cached_tarball}, re-verifying"
verify_binary "$node_impl" "$cached_tarball"
else
echo " --> cache miss: downloading ${binary_file}"
if [ "$node_impl" = "elements" ]; then
wget https://github.com/ElementsProject/elements/releases/download/${version}/${binary_file}
# elements publishes a clearsigned SHA256SUMS.asc only
( cd "./${node_impl}" && \
wget -q "https://github.com/ElementsProject/elements/releases/download/${version}/${binary_file}" && \
wget -q "https://github.com/ElementsProject/elements/releases/download/${version}/SHA256SUMS.asc" )
gpg_verify_sums "$node_impl" "./${node_impl}/SHA256SUMS.asc" "" \
|| { echo "ERROR: upstream GPG verification failed for elements SHA256SUMS.asc" >&2; exit 2; }
fi
if [ "$node_impl" = "bitcoin" ]; then
wget https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}
( cd "./${node_impl}" && \
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}" && \
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/SHA256SUMS" && \
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/SHA256SUMS.asc" )
gpg_verify_sums "$node_impl" "./${node_impl}/SHA256SUMS" "./${node_impl}/SHA256SUMS.asc" \
|| { echo "ERROR: upstream GPG verification failed for bitcoin SHA256SUMS.asc" >&2; exit 2; }
fi
# Primary trust anchor: committed tests/${node_impl}_SHA256SUMS.
verify_binary "$node_impl" "$cached_tarball"
fi
tar -xzf ${binary_file}
# Extract into ./${node_impl}-${version}/ (tarballs ship that layout).
# Leave the tarball in place inside ./${node_impl}/ to be reused on
# subsequent cache hits (that directory becomes a symlink below, so we
# copy the tarball out to a sibling and re-plant it after the symlink).
local tarball_basename="${binary_file}"
cp "$cached_tarball" "./${tarball_basename}.verified"
tar -xzf "$cached_tarball"
if [[ -d ./"$node_impl" ]]; then
if [[ -d ./"$node_impl"/src ]]; then
mv ./"$node_impl" ./"$node_impl"-src
@ -253,8 +467,12 @@ function sub_binary {
rm -rf ./"$node_impl"
fi
fi
rm "$node_impl"
rm -f "$node_impl"
ln -s ./"$node_impl"-${version} "$node_impl"
# Re-plant the verified tarball inside the (now-symlinked) cached dir so
# the next cache restore can re-verify it.
mv "./${tarball_basename}.verified" "./${node_impl}/${binary_file}"
echo " --> Listing binaries"
if [ $(uname) = "Darwin" ]; then
find ./"$node_impl"/bin -maxdepth 1 -type f -perm +111 -exec ls -ld {} \;
@ -263,6 +481,11 @@ function sub_binary {
fi
echo " --> checking for ${node_impl}d"
test -x ./${node_impl}/bin/${node_impl}d || exit 2
# Final defense-in-depth: re-verify the tarball one more time so that
# tampering between extract and exit is caught as well.
verify_binary "$node_impl" "./${node_impl}/${binary_file}"
echo " --> Finished installing ${node_impl}d binary"
END=$(date +%s)
DIFF=$(echo "$END - $START" | bc)