hashgraph / hashgraph/solo-weaver

feat(daemon): verify solo-provisioner CLI via embedded GPG primary key before privileged exec

Open
#698 1 comment 0 reactions 1 assignee Claimed by @leninmehedy View on GitHub
Needs Triage order: foundation
Dominant language
Go
Stars
3
Forks
0
Avg merge
3d 2h
Merged PRs (30d)
46

Description

## Context

Part of the **solo-provisioner-daemon core** epic (#499). Companion hardening to the
bin-dir permission fix (`/opt/solo/weaver/bin` → `root:root 0755`).

The unprivileged daemon (`User=weaver`) performs privileged work by exec'ing the
root `solo-provisioner` CLI via a NOPASSWD sudoers grant — whatever binary sits at
that path runs as root. The bin-dir perms fix stops a *weaver*-level actor from
swapping the binary, but we want the daemon to **prove the CLI it is about to run
as root is an authentic Hashgraph release**, defending against a tampered/MITM'd
self-upgrade download, a compromised mirror, or a binary swapped by some other
root-capable process — with **no outbound network call at verify time**.

Full design (trust model, key hierarchy, rotation, release-pipeline impact):
[`docs/dev/daemon/cli-verification-design.md`](../blob/main/docs/dev/daemon/cli-verification-design.md).

## Trust model (summary)

- The release pipeline already publishes GPG signatures (`.asc`,
`.sha256.asc`). The **GPG public key** — not the bare SHA — is the trust
anchor.
- Use a **long-lived primary key + rotatable signing subkeys**. The daemon/CLI embed
only the **primary public key** (the public half; the primary *private* key stays
offline/air-gapped and is never embedded). Signing subkeys are validated at runtime
by chaining to the embedded primary public key, so they rotate **without a daemon
upgrade**.
- Never bootstrap trust by fetching the verification key untrusted: a downloaded key
block is accepted only if its subkey binding chains to an embedded primary public
key.

## Scope

- New pure-Go package `pkg/codesign` (vendored `github.com/ProtonMail/go-crypto/openpgp`):
`VerifyKeyring` (chain-to-embedded-primary-public-key), `VerifyDetached`, `VerifyBinary`.
Embed primary **public** key(s) as a short ordered list via `//go:embed` (public key
material only). errorx errors carrying `ErrPropertyResolution`.
- **Install / self-upgrade gate** (`pkg/software/daemon_installer.go` + CLI self-upgrade):
download `.asc` + public key block, verify (chain → primary public key, then
signature), promote into the bin dir only on success, copy the verified `.asc` + key
block beside the binary, and record a `root:root 0644` manifest (`bin/.binaries.json`).
- **Daemon pre-exec hook**: a single `internal/daemon/cli` delegation chokepoint
`Run(ctx, args...)` that re-verifies the on-disk CLI's signature (full signature
re-verification, chains to embedded primary public key, **network-free**) before
`sudo`-exec'ing it; on failure refuses and surfaces a `StatusError`
(`CLIVerificationFailed`) in `/status`.
- **daemon.yaml**: add validated `cli_binary_path` (default
`/opt/solo/weaver/bin/solo-provisioner`). Do **not** store a checksum there
(`ConfigDir` is group-writable, so it is not a trust anchor).

## Release-pipeline changes (separate but required)

- Restructure the signing key into an **offline/air-gapped primary + CI signing
subkey**; export only the subkey to CI. The primary private key never reaches CI.
- Publish the **public key block** as a release asset (primary public key + current
subkey public keys with binding signatures); add to `.releaserc_cli.json` /
`.releaserc_daemon.json`.
- Keep direct binary signing (`.asc`). Export the **primary public key** into
the repo (public material only) and document the rotation runbooks (common subkey
rotation; rare primary rotation via cross-signed self-upgrade — roll verifiers/daemons
before cutting over signing; manual `daemon service install` as compromise recovery).

## Out of scope

- The daemon's concrete privileged-delegation actions (decommission, self-upgrade
command surface) — this issue only establishes the verified chokepoint they will
flow through.
- Standalone-module extraction of `pkg/codesign`.

## Acceptance criteria

- Daemon refuses to exec a CLI whose signature does not chain to an embedded primary
public key.
- Self-upgrade rejects a download whose signature does not verify, leaving the prior
binary in place.
- Signing-subkey rotation requires **no** daemon upgrade; primary rotation is
documented (cross-sign cutover) with a manual recovery path.
- No outbound network call on the daemon pre-exec path.
- Only public key material is embedded in the binaries (no private key).
- `pkg/codesign` is pure Go, runs in CI on macOS, new dep vendored; `task lint` passes.
- Unit coverage: valid sig passes; tampered binary fails; subkey not chaining to the
primary public key fails; rotated subkey under same primary passes; malformed/unbound
key fails.

## Dependencies

- Builds on the bin-dir `root:root 0755` hardening (same PR/epic).
- Requires the release-pipeline restructure (primary + subkey, published key block)
before the embedded anchor can be finalized.
- Confirm the current release key can be restructured into primary+subkeys and its
type/curve is supported by `ProtonMail/go-crypto`.

## Open questions

- Revocation for a years-running daemon (honor revocation certs in the downloaded key
block + a minimum subkey-creation-date floor).
- Whether the CLI should also self-verify on every privileged invocation, or only the
daemon verifies before delegating (proposed: daemon-only first).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.