Skip to content

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

Merged
sreerevanth merged 3 commits into
sreerevanth:mainfrom
Prateeks16:feat/license-enforcement-406
Jun 22, 2026
Merged

feat(security): RS256 entitlement verification for premium license enforcement (#406)#443
sreerevanth merged 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 client-side entitlement verification for premium access, including feature gating.
    • Added machine-bound entitlement support to prevent reuse on other machines.
    • Introduced typed license errors for clearer validation outcomes.
  • Chores
    • Updated cryptography/JWT dependency to support RS256 verification.
  • Tests
    • Added end-to-end tests covering valid/invalid tokens, expiration, claim validation, machine binding, and feature requirements.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d274f2-33b6-4a8e-b8fb-618f962a638b

📥 Commits

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

📒 Files selected for processing (3)
  • agentwatch/security/license.py
  • pyproject.toml
  • tests/test_license.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • pyproject.toml
  • 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
Entitlement data model and exception hierarchy
agentwatch/security/license.py
Defines LicenseError subclasses (LicenseUnavailableError, LicenseInvalidError, LicenseExpiredError, MachineMismatchError) for error reporting and handling. Adds the immutable Entitlement dataclass capturing verified JWT claims: subject, tier, expires_at, optional machine_id, and optional features (frozenset), plus a grants(feature) membership method.
Machine fingerprinting and token verification
agentwatch/security/license.py, pyproject.toml
Implements current_machine_id() using SHA-256 of hostname and MAC node value for stable machine identification. Implements verify_entitlement() with runtime PyJWT import, RS256 JWT decoding requiring sub, exp, and tier claims, explicit mapping of PyJWT exceptions to typed license errors, optional machine-binding validation against expected fingerprint, and claim type validation before constructing Entitlement with UTC-based expires_at. Adds pyjwt>=2.8.0 as an optional crypto extra.
Feature access enforcement
agentwatch/security/license.py
Implements require_feature() to gate feature access: raises LicenseInvalidError when no entitlement is provided or when entitlement.grants(feature) is false; otherwise returns the entitlement.
Test infrastructure and token signing helpers
tests/test_license.py
Adds module imports for datetime, PyJWT, RSA serialization, and license APIs. Provides a module-scoped keypair fixture generating fresh RSA private/public PEM pairs. Adds _make_token() helper to build JWT payloads with default claims (sub, tier, exp) and merge caller-supplied overrides before RS256 signing.
Token and machine binding verification tests
tests/test_license.py
Tests valid token parsing producing correct Entitlement with expected subject/tier and feature grants. Asserts expired JWTs raise LicenseExpiredError. Verifies tampered signatures and tokens signed with different keys raise LicenseInvalidError. Tests machine-binding: verification succeeds on machine_id match and raises MachineMismatchError on mismatch; unbound tokens bypass machine checking.
Claim validation and gating tests
tests/test_license.py
Tests missing required claim raises LicenseInvalidError. Validates claim-shape robustness: non-numeric exp, non-string tier, and non-list/non-set features all raise LicenseInvalidError. Tests current_machine_id() stability and 64-character alphanumeric sha256-hex format. Tests require_feature() success for granted features and failure for missing features or when called with None.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 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 27.78% 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 'feat(security): RS256 entitlement verification for premium license enforcement (#406)' accurately describes the main change: implementing RS256 JWT verification for premium license entitlements. It is specific, clear, and directly reflects the primary purpose of the changeset.
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
Contributor

🧪 PR Test Results

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

Python 3.12 · commit ed6055b

@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 added a commit to Prateeks16/AgentWatch that referenced this pull request Jun 19, 2026
…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>
Prateeks16 and others added 3 commits June 21, 2026 14:59
…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>
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>
@Prateeks16 Prateeks16 force-pushed the feat/license-enforcement-406 branch from c0aa682 to ed6055b Compare June 21, 2026 09:31
Prateeks16 added a commit to Prateeks16/AgentWatch that referenced this pull request Jun 21, 2026
…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>
@sreerevanth sreerevanth merged commit d098c10 into sreerevanth:main Jun 22, 2026
10 checks passed
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