Skip to content

feat(pallet-pass): per-device call filters#71

Open
olanod wants to merge 6 commits intomainfrom
device-filters
Open

feat(pallet-pass): per-device call filters#71
olanod wants to merge 6 commits intomainfrom
device-filters

Conversation

@olanod
Copy link
Copy Markdown
Member

@olanod olanod commented Apr 3, 2026

Summary

Adds a compact per-device call filter system to pallet-pass, enforcing least-privilege access control for authentication devices.

  • DeviceFilter enum with four variants:
    • Admin — unrestricted (first/recovery device only)
    • Pallets — whitelist by pallet index
    • Calls — whitelist by (pallet_index, call_index) pairs
    • Spend — asset transfer limits with per-asset max amounts
  • No-escalation invariant: a device can only grant permissions it already has (is_superset_of() check in add_device)
  • Call filtering in PassAuthenticate extension: checks device filter against the dispatched call before setting the origin
  • SpendMatcher trait: runtime-provided extraction of (asset_id, amount) from calls for Spend filter evaluation
  • First device via register() gets Admin; subsequent devices require explicit filter

Still TODO (will iterate)

  • Session key filter: mandatory filter at creation, no Admin allowed, filter check in session key path
  • Challenger improvements (replay resistance)

Test plan

  • 41 tests passing (34 existing + 7 new)
  • First device gets Admin filter
  • Admin can delegate any filter
  • Restricted device cannot escalate to Admin
  • Spend device cannot escalate to Calls/Pallets
  • Pallets ⊇ Calls within those pallets
  • Spend limit hierarchy (lower ok, higher rejected, wrong asset rejected)
  • Filter cleaned up on device removal
  • Full workspace compiles

Addresses virto-network/kreivo#479 (partial)

olanod added 6 commits April 3, 2026 20:08
Introduce DeviceFilter enum with four variants:
- Admin: unrestricted (first/recovery device)
- Pallets: whitelist by pallet index
- Calls: whitelist by (pallet_index, call_index) pairs
- Spend: asset transfer limits with per-asset max amounts

Key changes:
- New DeviceFilters storage map alongside Devices
- register() assigns Admin filter to first device
- add_device() takes caller_device + filter params with no-escalation check
- PassAuthenticate extension checks filter against call before dispatch
- SpendMatcher trait for runtime to provide asset transfer extraction
- DeviceFilter::is_superset_of() enforces privilege hierarchy

WIP: tests need updating for new add_device signature
All 34 existing tests pass with the new caller_device and filter
parameters. Existing tests use DeviceFilter::Admin to maintain
current behavior.
Tests for the no-escalation invariant and filter hierarchy:
- First device gets Admin filter on registration
- Admin can add device with any filter
- Restricted (Calls) device cannot escalate to Admin
- Spend device cannot escalate to Calls/Pallets
- Pallets filter is superset of Calls within those pallets
- Spend limit hierarchy (lower ok, higher rejected, wrong asset rejected)
- Filter is cleaned up on device removal
- add_session_key now takes a mandatory DeviceFilter parameter
- Admin filter rejected for session keys (PermissionEscalation error)
- Session key filter stored in SessionKeys storage and checked in
  PassAuthenticate extension before dispatching
- Filter stored as third element in SessionKeys tuple:
  (account, expiry, filter)

New tests:
- Session key cannot have Admin filter
- Session key stores and retrieves filter correctly
Critical fixes:
- Remove self-asserted caller_device param from add_device. The
  authenticated device_id is now cryptographically bound via
  AuthenticatedDevice transient storage, set by PassAuthenticate
  extension during prepare and cleared in post_dispatch.
- Remove Default impl for DeviceFilter (was Admin). Missing filter
  now returns error (deny by default) instead of granting full access.

Medium fixes:
- add_session_key now enforces no-escalation: the caller's device
  filter must be a superset of the session's filter.
- Extract call_indices() helper to deduplicate call index extraction.

The no-escalation check reads AuthenticatedDevice storage, which is
only set when authenticating through the PassAuthenticate extension.
Direct extrinsic calls in tests must set it explicitly.
Update all benchmark functions for the new add_device and add_session_key
signatures. Set AuthenticatedDevice transient storage before calls that
require no-escalation checks. Use non-Admin filter for session key benchmarks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant