Authority hardening: license auth, coverage truthfulness, webhook HMAC - #60
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 allexits 0 (all 8 checks) plus the bun frontend gate. DO NOT MERGE — security-sensitive license/entitlement/webhook code requires human review.#56 (CRITICAL) —
/license/refreshauth + entitlement-sourced planservices/api/src/{license.rs,lib.rs,main.rs}account_idis derived from the authenticated principal — the request body is never read for the authority decision (a body namingacct_victimis ignored).plan/featurescome fromresolve_entitlementsover the account's real billing state, not a hardcoded"pro". A free account receives aplan: "free"token withadvanced_reportsdisabled; an account with no billing state resolves to the free set (never Pro).LICENSE_SIGNING_KEYis set, and is safe (auth-gated) when set.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/gated/{feature}route is removed from the liveapi::app()(POST now 404s) — it was a pure function over HTTP, not an authority boundary.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 incoverage_manifest().feature_gate::gate_*test; two newxtaskregression 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. TheAdvancedReportsTauri-command entry is preserved.#58 (HIGH) — webhook real HMAC + fail-closed selection
services/api/src/{webhook.rs,lib.rs,main.rs},services/api/Cargo.toml, rootCargo.tomlStripeWebhookVerifier::verifyperforms real Stripe-scheme verification: parset=/v1=, recompute HMAC-SHA256 over"{timestamp}.{raw_body}"with the webhook secret, constant-time compare viahmac::Mac::verify_slice(subtle-backed; never==), and reject a stale timestamp (300s window) or malformed header/hex — fail-closed, no panics.build_routerselects the real verifier wheneverSTRIPE_WEBHOOK_SECRETis set and never falls back to the mock (mirrors the license env-gating). The mock stays for tests/dev without the secret.Bytes).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_secretrejects the old mock constant) +webhook.rsunit tests (wrong secret, stale ts, malformed header/hex).hmac/sha2are added toservices/apionly — the ADR-0002 crate-edge check and cargo-deny desktop bans stay green.