Skip to content

Authority hardening: license auth, coverage truthfulness, webhook HMAC - #60

Merged
AojdevStudio merged 1 commit into
mainfrom
feature/authority-hardening
Jun 6, 2026
Merged

Authority hardening: license auth, coverage truthfulness, webhook HMAC#60
AojdevStudio merged 1 commit into
mainfrom
feature/authority-hardening

Conversation

@AojdevStudio

Copy link
Copy Markdown
Owner

Closes #56
Closes #57
Closes #58

One coherent, additive change closing three findings from the cross-vendor (Forge / GPT-5.5) authority audit at main. Strict TDD; cargo xtask gate --scope all exits 0 (all 8 checks) plus the bun frontend gate. DO NOT MERGE — security-sensitive license/entitlement/webhook code requires human review.

#56 (CRITICAL) — /license/refresh auth + entitlement-sourced plan

services/api/src/{license.rs,lib.rs,main.rs}

  • The route is mounted behind auth: the caller is resolved from their bearer token, and account_id is derived from the authenticated principal — the request body is never read for the authority decision (a body naming acct_victim is ignored).
  • The token's plan/features come from resolve_entitlements over the account's real billing state, not a hardcoded "pro". A free account receives a plan: "free" token with advanced_reports disabled; an account with no billing state resolves to the free set (never Pro).
  • Env-gating preserved: the route still mounts only when LICENSE_SIGNING_KEY is set, and is safe (auth-gated) when set.
  • Tests: services/api/tests/license_refresh.rs (401 without token; account from principal not body; free→free, Pro→Pro from the engine; no-billing not granted Pro) + grep-anti-criteria (no hardcoded plan literal, no body account read).

#57 (HIGH) — coverage truthfulness

services/api/src/{feature_gate.rs,lib.rs}, xtask/src/coverage.rs, tests

  • The body-trusting /gated/{feature} route is removed from the live api::app() (POST now 404s) — it was a pure function over HTTP, not an authority boundary.
  • All seven baseline FeatureKeys are now backed by authenticated /gated-feature/{feature} gate tests that resolve entitlement server-side from the token (services/api/tests/feature_gate_authenticated.rs), each named in coverage_manifest().
  • The manifest no longer references any body-trusting feature_gate::gate_* test; two new xtask regression guards (no_baseline_backend_entry_points_at_a_body_trusting_gated_route, every_baseline_key_has_an_authenticated_backend_gate_test) keep it that way. The AdvancedReports Tauri-command entry is preserved.
  • Net effect: a paid baseline feature can no longer be "covered" by a test that posts its own entitlements body.

#58 (HIGH) — webhook real HMAC + fail-closed selection

services/api/src/{webhook.rs,lib.rs,main.rs}, services/api/Cargo.toml, root Cargo.toml

  • StripeWebhookVerifier::verify performs real Stripe-scheme verification: parse t=/v1=, recompute HMAC-SHA256 over "{timestamp}.{raw_body}" with the webhook secret, constant-time compare via hmac::Mac::verify_slice (subtle-backed; never ==), and reject a stale timestamp (300s window) or malformed header/hex — fail-closed, no panics.
  • The binary's build_router selects the real verifier whenever STRIPE_WEBHOOK_SECRET is set and never falls back to the mock (mirrors the license env-gating). The mock stays for tests/dev without the secret.
  • Verified over the exact raw request bytes (verify-before-parse, same Bytes).
  • Tests: services/api/tests/stripe_webhook.rs (valid signed event reconciles; forged/tampered rejected with no state effect; idempotency holds with the real verifier; app_with_stripe_secret rejects the old mock constant) + webhook.rs unit tests (wrong secret, stale ts, malformed header/hex).
  • hmac/sha2 are added to services/api only — the ADR-0002 crate-edge check and cargo-deny desktop bans stay green.

Closes #56: /license/refresh now resolves the principal from the bearer
token (never the body) and sources the token plan/features from
resolve_entitlements — a free account gets a free token, not Pro.

Closes #57: removes the body-trusting /gated/{feature} route from the live
app and re-points the feature-key coverage manifest at authenticated
/gated-feature tests for all seven baseline keys, so no body-posting test
can earn backend coverage.

Closes #58: StripeWebhookVerifier::verify does real t=/v1= HMAC-SHA256 over
'{timestamp}.{payload}' with constant-time verify_slice and timestamp
staleness rejection; the binary selects the real verifier whenever
STRIPE_WEBHOOK_SECRET is set and never falls back to the mock. hmac/sha2 are
api-only (ADR-0002).

cargo xtask gate --scope all exits 0 (all 8 checks).
@AojdevStudio
AojdevStudio merged commit df6de23 into main Jun 6, 2026
2 checks passed
@AojdevStudio
AojdevStudio deleted the feature/authority-hardening branch June 6, 2026 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant