fix(app-store): verify manifest Store.Signature cryptographically (PILOT-98)#5
Conversation
…LOT-98) manifest.Validate() checked Store.Signature only for non-empty — the ed25519 signature over (publisher||id||manifest_version||binary.sha256||grants-hash) was never cryptographically verified, making it a decorative no-op. This adds manifest.VerifySignature() which decodes the ed25519 publisher key from Store.Publisher, recomputes the signing payload, and calls crypto/ed25519.Verify. The publisher key is included in the payload so swapping keys invalidates the signature. A future trust-anchor check (verifying Store.Publisher against a daemon-embedded key) will complete the chain. scanInstalled() now calls VerifySignature() after Validate(), rejecting manifests with invalid signatures at discovery time. Closes PILOT-98
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
🤖 Matthew PR Check — #5 PILOT-98Status
Files changed: 4 (+197/−9)
|
🤖 Matthew — Code Walkthrough (#5, PILOT-98)What this doesThe File-by-file
What this does NOT do (yet)This does NOT verify that |
…5) (#3) The install flow had no monotonic version check — re-installing an older app_version on top of a newer one succeeded silently. This is a classic rollback-attack vector: a CVE patched in v1.2 can be re-introduced by reinstalling v1.1. This adds: - compareVersions() — basic semver comparison (MAJOR.MINOR.PATCH[-PRE]) - registerInstalled() now refuses to replace an in-memory entry with a lower app_version for the same app ID (startup path). - rescanForNew() now detects on-disk manifest version changes for already-known apps and refuses downgrades; upgrades are accepted and trigger a clean restart of the supervise goroutine. - Audit events (downgrade-refused) in the supervisor log for forensics. The check is best-effort defense-in-depth — full protection requires signed manifests (PILOT-98, the #5 dependency) to prevent an attacker from forging the app_version field alongside the binary. Closes PILOT-105 Co-authored-by: matthew-pilot <matthew-pilot@vulturelabs.io>
What
manifest.Validate() checked
Store.Signatureonly for non-empty — the ed25519 signature over(publisher||id||manifest_version||binary.sha256||grants-hash)was never cryptographically verified, making it a decorative no-op. An attacker who can write to an app dir could hand-edit grants and the daemon would accept the manifest as valid.Fix
Adds
manifest.VerifySignature()which:Store.Publisher(formatted as"ed25519:<base64>")crypto/ed25519.VerifyagainstStore.SignatureThe publisher key is included in the signing payload so swapping keys invalidates the signature.
scanInstalled()now callsVerifySignature()afterValidate(), rejecting manifests with invalid signatures at discovery time.Files
Verification
Note
This does NOT check that
Store.Publishermatches a trusted publisher key — that trust-anchor check is the natural follow-up hardening step. For now, this makes the signature field mean something (it must be a valid ed25519 signature over canonical manifest content), preventing unauthenticated tampering.Closes PILOT-98