Skip to content

feat(security): RS256 entitlement verification for premium license enforcement (#406)#443

Open
Prateeks16 wants to merge 3 commits into
sreerevanth:mainfrom
Prateeks16:feat/license-enforcement-406
Open

feat(security): RS256 entitlement verification for premium license enforcement (#406)#443
Prateeks16 wants to merge 3 commits into
sreerevanth:mainfrom
Prateeks16:feat/license-enforcement-406

Conversation

@Prateeks16

@Prateeks16 Prateeks16 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Closes part of #406 (client-side verification slice).

What

Adds the client-side verification primitive for premium entitlements. A premium entitlement is a JWT signed by the backend with an RS256 private key and verified by the CLI against the matching public key. Because signing is asymmetric, the client only ever holds the public key and cannot mint its own tokens — premium gating becomes a signature check rather than a patchable if is_premium() boolean.

Changes

  • agentwatch/security/license.py
    • verify_entitlement(token, public_key, *, machine_id=None) — RS256-only decode, mandatory sub/exp/tier claims, expiry enforcement, and optional device binding. Fails closed (LicenseUnavailableError) when PyJWT is absent rather than failing open.
    • current_machine_id() — stable, hashed device fingerprint for binding tokens to a machine (enables the backend to flag concurrent use across unlinked devices).
    • require_feature(entitlement, feature) — entitlement-gated feature check; no bare boolean to strip.
    • Typed exception hierarchy under LicenseError.
  • pyjwt added to the optional crypto extra (cryptography already present).
  • 9 unit tests: valid/expired/tampered/wrong-key/missing-claim/machine-binding/gate.

Acceptance criteria mapping (#406)

  • Premium license tokens are cryptographically verified (RS256, asymmetric).
  • Device/hardware binding primitive in place (machine_id claim + current_machine_id()).
  • No trivial boolean gate — premium paths gate on a verified entitlement.
  • Full server-side enforcement and token issuance require the backend/payment flow tracked in [CRITICAL] Implement CLI-to-Web Monetization Flow & Payment Gateway Integration #405; out of scope here.

Test

python -m pytest tests/test_license.py -q   # 9 passed
ruff check                                   # clean

Per the issue's note, this PR keeps to the defensive implementation and omits any bypass-oriented detail.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added license entitlement verification system with feature-gating capabilities.
    • Added support for machine-binding in license validation.
    • Added comprehensive error handling for invalid, expired, or mismatched licenses.
  • Chores

    • Updated dependencies for token verification support.

…forcement (sreerevanth#406)

Add the client-side verification primitive for premium entitlements:
a JWT signed by the backend with an RS256 private key, verified by the
CLI against the matching public key. Asymmetric signing means the client
holds only the public key and cannot mint its own tokens, so premium
gating becomes a signature check rather than a patchable boolean.

- verify_entitlement(): RS256-only decode, required claims, expiry, and
  optional machine binding; fails closed if PyJWT is unavailable.
- current_machine_id(): stable, hashed device fingerprint for binding.
- require_feature(): entitlement-gated feature check (no bare boolean).
- pyjwt added to the optional `crypto` extra.

Scope: client verification + device binding only. Server-side
enforcement and token issuance are tracked in sreerevanth#405.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 19, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Prateeks16, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 37 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c7e013a4-c920-4c55-9fc1-a94289bb0630

📥 Commits

Reviewing files that changed from the base of the PR and between 6fdc0ef and c0aa682.

📒 Files selected for processing (2)
  • agentwatch/security/license.py
  • tests/test_license.py
📝 Walkthrough

Walkthrough

Introduces agentwatch/security/license.py, a new module implementing RS256 JWT-based premium entitlement verification. It defines a LicenseError exception hierarchy, an Entitlement dataclass, machine fingerprinting via current_machine_id(), token verification via verify_entitlement(), and feature gating via require_feature(). A pyjwt>=2.8.0 optional dependency is added, and a full pytest test suite is included.

Changes

Premium License Entitlement Enforcement

Layer / File(s) Summary
License module: exceptions, Entitlement, and verification logic
pyproject.toml, agentwatch/security/license.py
Adds pyjwt>=2.8.0 as an optional crypto extra. Defines LicenseError subclasses (LicenseUnavailableError, LicenseInvalidError, LicenseExpiredError, MachineMismatchError), the Entitlement dataclass with grants(), current_machine_id() using SHA-256 of hostname and MAC node, verify_entitlement() with runtime PyJWT import and RS256 decoding with claim/expiry/binding error mapping, and require_feature() for entitlement-guarded feature access.
Test suite: token signing, verification, machine binding, and feature gating
tests/test_license.py
Module-scoped RSA keypair fixture and _make_token helper sign controlled JWTs. Tests cover valid token parsing, expired/tampered/wrong-key rejection, machine-binding match and mismatch, unbound-token bypass, missing required claim, current_machine_id() stability and sha256 hex format, and require_feature() success and failure paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐇 A JWT hops in, signed with RS256 flair,
The rabbit checks the claims — subject, tier, expire.
Machine ID matched? The feature gate swings wide!
Tampered or expired? LicenseInvalidError inside.
Bindings hold firm, no sharing tokens here —
Premium entitlements, guarded crystal clear! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: implementing RS256 entitlement verification for premium license enforcement, which directly aligns with the PR's primary objective of adding client-side license verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

🧪 PR Test Results

Check Result
Tests (pytest tests/) ❌ failure
Lint (ruff check .) ✅ success
Coverage (agentwatch) 72.83%

Python 3.12 · commit c0aa682

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_license.py (1)

122-133: ⚡ Quick win

Add malformed-claim type regression tests.

Coverage is strong, but there’s no case for wrong claim types (e.g., features=123 or exp="tomorrow"). Add assertions that these reject with LicenseInvalidError to protect the error-contract behavior.

Also applies to: 142-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_license.py` around lines 122 - 133, Add additional test cases to
cover malformed claim types beyond just missing claims. Create new test
functions that verify the verify_entitlement function properly rejects tokens
with incorrect claim types, such as features being an integer instead of the
expected type, or exp being a string like "tomorrow" instead of a proper
timestamp. Each test should follow the same pattern as
test_missing_required_claim_rejected by creating a token with the malformed
claim type and asserting that verify_entitlement raises LicenseInvalidError with
the public key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agentwatch/security/license.py`:
- Around line 133-139: Before constructing the Entitlement object, add type
validation for the claims that are assumed to have specific types. Validate that
claims["exp"] is a numeric timestamp (int or float) before passing it to
datetime.fromtimestamp, and validate that claims["features"] is an iterable of
strings before passing it to frozenset. If either validation fails, raise
LicenseInvalidError instead of allowing the underlying type error to propagate
uncaught. This ensures the function maintains its typed error contract and
provides clear validation failures at the boundary.

---

Nitpick comments:
In `@tests/test_license.py`:
- Around line 122-133: Add additional test cases to cover malformed claim types
beyond just missing claims. Create new test functions that verify the
verify_entitlement function properly rejects tokens with incorrect claim types,
such as features being an integer instead of the expected type, or exp being a
string like "tomorrow" instead of a proper timestamp. Each test should follow
the same pattern as test_missing_required_claim_rejected by creating a token
with the malformed claim type and asserting that verify_entitlement raises
LicenseInvalidError with the public key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd9f2d61-1882-4df4-9aa3-3c0b621ec4b2

📥 Commits

Reviewing files that changed from the base of the PR and between e3012c5 and 6fdc0ef.

📒 Files selected for processing (3)
  • agentwatch/security/license.py
  • pyproject.toml
  • tests/test_license.py

Comment thread agentwatch/security/license.py
Prateeks16 and others added 2 commits June 19, 2026 22:33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lement (sreerevanth#406)

Guard the 'sub'/'tier'/'features' claims so a malformed token raises
LicenseInvalidError at the boundary instead of an uncaught TypeError,
preserving the typed error contract. (exp is already validated as numeric
by PyJWT during decode.) Adds regression tests for non-numeric exp,
non-string tier, and non-iterable features.

Addresses CodeRabbit review feedback on sreerevanth#443.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants