mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
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:
parent
3e277689ef
commit
a24df2eb50
8 changed files with 1070 additions and 6 deletions
215
.github/workflows/test.yml
vendored
Normal file
215
.github/workflows/test.yml
vendored
Normal 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
2
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,5 +23,9 @@
|
|||
"mochaFile": "cypresstest-output.xml",
|
||||
"toConsole": true
|
||||
},
|
||||
"experimentalSessionAndOrigin": true
|
||||
"experimentalSessionAndOrigin": true,
|
||||
"retries": {
|
||||
"runMode": 1,
|
||||
"openMode": 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
561
docs/cirrus-replacement-spec.md
Normal file
561
docs/cirrus-replacement-spec.md
Normal 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
35
tests/bitcoin_SHA256SUMS
Normal 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
22
tests/elements_SHA256SUMS
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue