Skip to content

MF3-M05: allow wallet-only revocation of session keys#809

Open
philanton wants to merge 2 commits into
fix/audit-findings-finalx3from
fix/mf3-m05
Open

MF3-M05: allow wallet-only revocation of session keys#809
philanton wants to merge 2 commits into
fix/audit-findings-finalx3from
fix/mf3-m05

Conversation

@philanton
Copy link
Copy Markdown
Contributor

@philanton philanton commented Jun 2, 2026

MF3-M05: wallet-only session-key revocation

Problem

SubmitSessionKeyState() (both app-session and channel paths) treated a submitted expires_at not after now as a revocation, but still required and validated session_key_sig before storing. A user could not unilaterally revoke a delegated key: if the session key was lost, unavailable, compromised, or held by a delegate who refused to sign the revocation payload, the user had to wait until the existing expires_at passed while the key stayed usable. Requiring session_key_sig is correct when activating or extending authority, but on revocation it handed the delegate a veto.

Fix

Split the signature requirement by intent:

  • expires_at > now (activation / extension / rotation) — still requires both user_sig and session_key_sig, validated as before.
  • expires_at <= now (revocation) — requires only user_sig. Any session_key_sig present is ignored.

The packed payload binds user_address, session_key, version, and expires_at, so a wallet-only revocation is authorized for exactly that key/version and cannot be replayed. Revocation still flows through LockSessionKeyState (ownership binding) and the monotonic version check.

Changes

  • pkg/app, pkg/core — new ValidateAppSessionKeyStateUserSigV1 / ValidateChannelSessionKeyStateUserSigV1; the both-signature validators now reuse them.
  • nitronode handlers — branch on expires_at to pick the validator. now is captured once and reused for the in-transaction cap/version logic so the active/inactive decision is consistent across validation and storage.
  • Migration 20260602000000 — drops the *_session_key_sig_present_chk CHECK constraints added in 20260508000000 (they rejected an empty session_key_sig). The ownership guarantee is preserved in application code: active rows are still co-signed, and owner/auth lookups filter expires_at > now, so revocation rows with an empty signature are never trusted as a key's owner. Down re-adds the constraints as NOT VALID.
  • SDK — wallet-only helpers: RevokeChannelSessionKey / RevokeAppSessionKey (Go), revokeChannelSessionKey / revokeSessionKey (TS). Updated the docstrings that previously stated wallet-only revocation was unsupported.
  • docs/api.yamlsession_key_sig documented as optional on the revocation path for both methods.

Tests

  • pkg-level unit tests for the new user-sig-only validators (valid passes, wrong wallet rejected, tampered version rejected).
  • Handler tests per kind: wallet-only revoke succeeds; a mismatched session_key_sig is ignored on revoke; an invalid user_sig is still rejected on revoke. Existing both-signature, cap, and reactivation tests unchanged and passing.

Verification

  • go build ./..., go vet (affected packages), Go tests for pkg/app, pkg/core, both handlers, sdk/go — pass.
  • TS typecheck + lint clean; 184 tests pass (public-API drift snapshot updated for the two new methods — intentional).

Note for reviewers

Migration is timestamped 20260602000000. If it should slot before another unreleased migration, say so and I'll rename.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added wallet-only session-key revocation support. New methods RevokeChannelSessionKey, RevokeAppSessionKey (Go SDK) and revokeChannelSessionKey, revokeSessionKey (TypeScript SDK) enable simplified revocation without requiring session-key holder signatures.
  • Documentation

    • Clarified signature requirements for session-key operations: activation/extension requires both wallet and session-key signatures; revocation requires wallet signature only.

MF3-M05: SubmitSessionKeyState required session_key_sig on every submit,
including revocation (expires_at <= now). This let a lost, unavailable, or
malicious delegate veto revocation of its own delegation — the user had to
wait for the prior expires_at to pass while the key stayed usable.

Revocation now validates user_sig alone; activation, extension, and rotation
(expires_at > now) still require both user_sig and session_key_sig. A
session_key_sig present on the revocation path is ignored.

- pkg/app, pkg/core: add ValidateAppSessionKeyStateUserSigV1 /
  ValidateChannelSessionKeyStateUserSigV1; refactor the both-signature
  validators to reuse them.
- nitronode handlers: branch on expires_at to pick the validator; capture
  now once and reuse it for the in-tx cap/version logic.
- migration: drop the session_key_sig non-empty CHECK constraints added in
  20260508000000. Active rows stay co-signed via app code, and owner/auth
  lookups filter expires_at > now, so revocation rows are never trusted.
- sdk/go, sdk/ts: add wallet-only RevokeChannelSessionKey / RevokeAppSessionKey
  (revokeChannelSessionKey / revokeSessionKey) helpers.
- docs/api.yaml: document session_key_sig as optional for revocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70a18df6-7227-449b-a9df-a671ceb635c0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements wallet-only session-key revocation. When expires_at <= now, revocation requires only the wallet signature; when expires_at > now, activation/update requires both wallet and session-key signatures. Core validators are refactored to isolate user-signature validation, API handlers branch on expiration time, database constraints are relaxed, and public SDK methods are added for callers.

Changes

Session-key wallet-only revocation

Layer / File(s) Summary
Core user-signature validators
pkg/core/session_key.go, pkg/core/session_key_test.go, pkg/app/session_key_v1.go, pkg/app/session_key_v1_test.go
New ValidateChannelSessionKeyStateUserSigV1 and ValidateAppSessionKeyStateUserSigV1 functions validate only wallet signature for revocation. Existing full validators delegate to these for user-signature verification, then continue with session-key signature validation.
Channel API revocation handler
nitronode/api/channel_v1/submit_session_key_state.go, nitronode/api/channel_v1/submit_session_key_state_test.go
SubmitSessionKeyState branches on expires_at relative to current time: future dates require both signatures via full validator, past/present dates require only user signature via user-only validator. Three test cases cover revocation with user signature only, ignored mismatched session-key signature, and invalid user signature rejection.
App API revocation handler
nitronode/api/app_session_v1/submit_session_key_state.go, nitronode/api/app_session_v1/submit_session_key_state_test.go
Parallel implementation to channel handler: branches validation based on expires_at expiration, routes to user-sig-only path for revocation and full validation for activation. Three test cases mirror channel handler coverage.
Database schema migration
nitronode/config/migrations/postgres/20260602000000_session_key_user_only_revocation.sql
Drops CHECK constraints enforcing non-empty session_key_sig on both app_session_key_states_v1 and channel_session_key_states_v1 tables to allow user-only revocation rows. Down migration reintroduces constraints marked NOT VALID.
Go SDK revocation methods
sdk/go/channel.go, sdk/go/app_session.go
Adds RevokeChannelSessionKey and RevokeAppSessionKey to client. Methods enforce expires_at <= now, sign with wallet signature only, and submit via existing submit methods. Updates submit method documentation to clarify revocation vs. activation requirements.
TypeScript SDK revocation methods
sdk/ts/src/client.ts
Adds revokeChannelSessionKey and revokeSessionKey methods implementing wallet-only revocation flow: validate expiration, sign with wallet, clear session-key signature, and submit. Updates submit method JSDoc to direct users to dedicated revocation methods.
API documentation
docs/api.yaml
Clarifies signature requirements in type and endpoint documentation. Specifies both signatures required for activation/update, only user signature for revocation (expires_at <= now), and that session-key signature is ignored on revocation path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • layer-3/nitrolite#739: Previously required session_key_sig on all submits including revokes; this PR directly modifies that behavior to ignore session_key_sig for revocation.
  • layer-3/nitrolite#775: Also modifies submit_session_key_state handlers to branch on expires_at, treating past expiration as revocation.

Suggested reviewers

  • dimast-x
  • nksazonov

🐰 A rabbit hops through time's narrow gate,
One signature to seal the fate,
When keys grow old, they fade to dust,
The wallet's word is all we trust. 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.87% 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 clearly and specifically describes the main change: enabling wallet-only revocation of session keys, which is the central feature of this PR.
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
  • Commit unit tests in branch fix/mf3-m05

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/api.yaml (1)

389-397: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mark session_key_sig optional in the schema too.

The prose now allows wallet-only revocation, but both type definitions still model session_key_sig as required. Anything generated or validated from docs/api.yaml will keep rejecting valid revocation requests that omit the field.

Suggested fix
         - name: session_key_sig
           type: string
+          optional: true
           description: >
             Session-key holder's EIP-191 signature over the same payload as user_sig —
             abi.encode(SESSION_KEY_AUTH_TYPEHASH, session_key, metadata_hash) — proving possession of the key
             being registered. Required when expires_at is in the future (registration, update, re-activation)
             to prevent registration of keys the submitter does not control. Optional for revocation
             (expires_at at or before now): a wallet may revoke unilaterally with user_sig alone so a lost or
             uncooperative session key cannot block revocation.
         - name: session_key_sig
           type: string
+          optional: true
           description: Session-key holder's signature over the same packed state (which already binds user_address) proving possession of the key being registered. Required when expires_at is in the future (registration, update, re-activation) to prevent registration of keys the submitter does not control. Optional for revocation (expires_at at or before now); a wallet may revoke unilaterally with user_sig alone so a lost or uncooperative session key cannot block revocation.

As per coding guidelines, "docs/api.yaml: API definition and canonical list of all v1 RPC methods, types, and request/response schemas is located in docs/api.yaml."

Also applies to: 427-429

🤖 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 `@docs/api.yaml` around lines 389 - 397, The schema currently marks
session_key_sig as required; update the docs/api.yaml type/request schemas that
include the session_key_sig field so it is optional (remove it from any
"required" lists or change its required flag), since revocation requests where
expires_at is at-or-before now must be allowed without session_key_sig; apply
the same change to the other occurrence(s) of session_key_sig in the file (the
second block referenced around the later occurrence) and ensure the field
description still documents that session_key_sig is required for future-dated
registrations/updates/reactivations but optional for revocation when expires_at
<= now.
🤖 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 `@docs/api.yaml`:
- Around line 674-676: The field description for the ChannelSessionKey schema
(`field_name: state`, `type: channel_session_key_state`) incorrectly says "past
value" whereas the server allows expires_at == now; update the description text
to say "at or before now" (e.g., "Set `expires_at` to at or before now to revoke
(user_sig only); future to (re-)activate (both signatures).") and make the same
change for the other identical occurrence of this field description elsewhere in
the file so both places match the server contract.

In `@nitronode/api/app_session_v1/submit_session_key_state.go`:
- Around line 114-120: The revocation branch currently validates only the user
signature but then persists the unchanged coreState, allowing an unvalidated
session_key_sig to be stored; update the branch in submit_session_key_state.go
(where ValidateAppSessionKeyStateUserSigV1(coreState) is called) to clear the
SessionKeySig field on coreState (set to zero value) before returning/persisting
so revocation rows never retain client-provided session_key_sig and stored state
matches the public contract.

In `@nitronode/api/channel_v1/submit_session_key_state.go`:
- Around line 97-103: The revoke branch currently ignores but does not clear
coreState.SessionKeySig, allowing an unverified signature to be persisted;
update the revocation path in submit_session_key_state.go so that after calling
ValidateChannelSessionKeyStateUserSigV1(coreState) you explicitly zero/clear
coreState.SessionKeySig before any persistence, and add a regression assertion
in the revoke unit test to confirm the stored/returned state has an empty
SessionKeySig; reference the coreState struct, its SessionKeySig field, the
ValidateChannelSessionKeyStateUserSigV1 check, and the code path that persists
coreState to locate where to clear the field and where to assert in tests.

In `@sdk/go/channel.go`:
- Around line 1101-1103: RevokeChannelSessionKey's time check uses nanosecond
precision (state.ExpiresAt.After(time.Now())) but signing uses Unix seconds, so
change the guard to compare Unix-second timestamps to match
SignChannelSessionKeyState / SignChannelSessionKeyOwnership: compare
state.ExpiresAt.Unix() against time.Now().Unix() (e.g., require
state.ExpiresAt.Unix() <= time.Now().Unix()) so values that serialize to the
current Unix second are not incorrectly rejected.

---

Outside diff comments:
In `@docs/api.yaml`:
- Around line 389-397: The schema currently marks session_key_sig as required;
update the docs/api.yaml type/request schemas that include the session_key_sig
field so it is optional (remove it from any "required" lists or change its
required flag), since revocation requests where expires_at is at-or-before now
must be allowed without session_key_sig; apply the same change to the other
occurrence(s) of session_key_sig in the file (the second block referenced around
the later occurrence) and ensure the field description still documents that
session_key_sig is required for future-dated registrations/updates/reactivations
but optional for revocation when expires_at <= now.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ce11d05-5b5b-49bb-a8ed-132ddd84b48a

📥 Commits

Reviewing files that changed from the base of the PR and between 0c46c9e and bc33ba9.

⛔ Files ignored due to path filters (1)
  • sdk/ts/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (13)
  • docs/api.yaml
  • nitronode/api/app_session_v1/submit_session_key_state.go
  • nitronode/api/app_session_v1/submit_session_key_state_test.go
  • nitronode/api/channel_v1/submit_session_key_state.go
  • nitronode/api/channel_v1/submit_session_key_state_test.go
  • nitronode/config/migrations/postgres/20260602000000_session_key_user_only_revocation.sql
  • pkg/app/session_key_v1.go
  • pkg/app/session_key_v1_test.go
  • pkg/core/session_key.go
  • pkg/core/session_key_test.go
  • sdk/go/app_session.go
  • sdk/go/channel.go
  • sdk/ts/src/client.ts

Comment thread docs/api.yaml Outdated
Comment thread nitronode/api/app_session_v1/submit_session_key_state.go
Comment thread nitronode/api/channel_v1/submit_session_key_state.go
Comment thread sdk/go/channel.go Outdated
@philanton philanton changed the base branch from main to fix/audit-findings-finalx3 June 2, 2026 14:42
Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham left a comment

Choose a reason for hiding this comment

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

Good direction overall: future-dated registration and re-activation still require both signatures, and the new user-only revoke path addresses the lost or uncooperative session-key case.

I would keep this open for now. The remaining blocker is that an expired first submit can reserve an unowned session_key before any delegate signature is required. I left inline notes on both handler paths.

Also worth picking up the existing notes on clearing SessionKeySig before storing revocation rows, marking session_key_sig optional in the API schema, and aligning the Go helper time check to Unix seconds.

c.Fail(rpc.Errorf("invalid_session_key_state: %v", err), "")
return
}
} else {
Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham Jun 3, 2026

Choose a reason for hiding this comment

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

Good direction on splitting active vs revoke validation, but I would block on one ownership edge case here. A wallet can submit version=1 with expires_at <= now for any arbitrary session_key; this branch only checks user_sig, then LockSessionKeyState creates the permanent (session_key, kind) owner row and the later latestVersion + 1 check accepts it.

Could we only allow the user-only path after the lock proves latestVersion > 0 for this wallet? For first submit, reject the expired state or require both signatures before the pointer row can be created.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good catch — this was the real hole. Fixed in bf08526.

I went with rejecting a revoke at version == 1 before LockSessionKeyState runs, rather than gating on latestVersion > 0 after the lock. The reason: the seed row is written inside the same tx, so a version >= 2 revoke on an unowned key would seed, then fail the latestVersion + 1 check and roll the seed back with the tx. The only revoke that can actually commit a claim for a key with no prior delegation is version == 1 (passes 0 + 1), so blocking that is enough — and it never even seeds. A legitimate revoke is always version >= 2 since registration at v1 is future-dated and requires both sigs.

c.Fail(rpc.Errorf("invalid_session_key_state: %v", err), "")
return
}
} else {
Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham Jun 3, 2026

Choose a reason for hiding this comment

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

Same issue applies here for app session keys. A first submit with expires_at <= now can reserve an unowned session_key under the caller because the user-only branch runs before the lock proves an existing delegation.

Please gate wallet-only revocation on latestVersion > 0. First submits should either be future-dated with both signatures or rejected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same fix here — bf08526. Reject version == 1 revoke before the lock, same reasoning as the channel handler (the seed only persists if the tx commits, and only a v1 revoke commits without a prior delegation).

Comment thread sdk/ts/src/client.ts Outdated
*
* @param state - The channel session key state to revoke (version = latest + 1, expires_at <= now)
*/
async revokeChannelSessionKey(state: ChannelSessionKeyStateV1): Promise<void> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This helper signs and clears the signature fields itself, but the input type still requires callers to provide user_sig and session_key_sig. That means TS callers need dummy values for the wallet-only revoke path.

Could we use a dedicated revoke input type, or Omit<ChannelSessionKeyStateV1, 'user_sig' | 'session_key_sig'>, and apply the same shape to revokeSessionKey?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, makes sense — switched both revokeChannelSessionKey and revokeSessionKey to Omit<…, 'user_sig' | 'session_key_sig'> so callers don't pass dummy sigs. bf08526

Comment thread sdk/go/app_session.go Outdated
// }
// err := client.RevokeAppSessionKey(ctx, state)
func (c *Client) RevokeAppSessionKey(ctx context.Context, state app.AppSessionKeyStateV1) error {
if state.ExpiresAt.After(time.Now()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same precision issue applies to the app helper. This guard uses full time.Time precision, but PackAppSessionKeyStateV1 signs ExpiresAt.Unix(), so a value inside the current Unix second can be rejected locally even though the server treats it as expires_at <= now.

Please compare Unix seconds here too, matching the channel helper fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in bf08526 — comparing ExpiresAt.Unix() here too, matching the channel helper.

…on rows

Address PR review on wallet-only session-key revocation:

- Reject a revoke at version 1 in both submit handlers before LockSessionKeyState
  runs. A version-1 revoke has no prior delegation, so it would otherwise let a
  wallet seed a permanent (session_key, kind) ownership claim for a key it never
  proved possession of. A legitimate revoke is always version >= 2.
- Clear SessionKeySig on the revocation path before persisting so stored revoke
  rows never retain unverified client input.
- Compare Unix seconds in RevokeChannelSessionKey / RevokeAppSessionKey guards to
  match the precision the metadata hash is signed at, so a value inside the
  current Unix second is not rejected locally.
- Use Omit<..., 'user_sig' | 'session_key_sig'> for the TS revoke method inputs
  so callers no longer pass dummy signature fields.
- Fix docs/api.yaml field descriptions ("past value" -> "at or before now") and
  correct the stale seed-row permanence comment, which claimed the seed survives
  aborted transactions.

Adds regression tests for the version-1 rejection and the cleared SessionKeySig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham left a comment

Choose a reason for hiding this comment

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

Approving for M-05. The latest commit closes the revoke-veto path and the first-submit ownership issue: v1 revokes are rejected before the pointer lock, v2+ first-submit revokes fail the version check in the transaction, and revoke rows are canonicalized by clearing SessionKeySig.

Only non-blocking follow-up I would consider is a DB-backed regression for the v2+ first-submit rollback case, since the current tests already cover the v1 pre-lock rejection.

@nksazonov
Copy link
Copy Markdown
Contributor

nksazonov commented Jun 4, 2026

After reading this PR, I have some thoughts:

Design question: who should be allowed to revoke a session key?

What the PR does now

Revocation (expires_at <= now) requires only user_sig (wallet owner). Any session_key_sig present is silently ignored and cleared before storage.


Industry analogues

Domain Who can revoke? Notes
OAuth 2.0 RFC 7009 Owner OR token holder Token surrender is an explicit, recommended pattern — client calls /revoke authenticated with its own token.
GitHub PATs Owner OR key itself DELETE /applications/{client_id}/grant works with the token as auth.
JWT Owner only (server-side) JWTs can't self-revoke — but they're stateless. Not analogous since we have a DB.
SSH authorized_keys Owner only Key can't deauthorize itself, but SSH keys are passive and don't run code.
Power of Attorney (banking) Principal OR attorney Attorney can resign ("surrender") independently of the principal revoking.

The closest analogue is OAuth RFC 7009. A session key is a delegated access token that runs code — exactly what RFC 7009 covers.


The core argument for EITHER

A session key self-revoking is strictly a reduction of authority. It can never escalate privilege. This makes it unconditionally safe.

Practical use cases the current design blocks:

  • Autonomous agent graceful shutdown — a bot that finishes a job wants to clean up its own delegation without requiring the wallet to come online.
  • Compromised key first-response — a leaked session key can self-revoke before the attacker uses it (the attacker can't delay revocation by refusing to sign).
  • SDK ergonomicsRevokeSessionKey() that works from the session key side, not just the wallet side. Much more natural for server-to-server integrations.

Security consideration on session-key-initiated revocation

Could an attacker who steals a session key cause harm by self-revoking it?

  • Self-revocation only removes authority — the attacker gains nothing.
  • Worst case: the user has to submit version + 1 to re-activate (a minor inconvenience).
  • The signed payload already binds (user_address, session_key, version, expires_at) — no replay across keys, wallets, or versions.

The one non-obvious risk is a racing revocation: an attacker steals the key and immediately self-revokes it before the owner notices. But this is a minor DoS at most — the owner can re-register at version + 1. And if the key is already stolen, the user has bigger problems regardless.


My personal recommendation: accept EITHER user_sig OR valid session_key_sig on revocation

Logic for the revocation branch:

  1. If session_key_sig is present and valid → accept (session key surrenders itself).
  2. Else if user_sig is valid → accept (owner forcibly revokes).
  3. Else → reject.

user_sig still remains the escape hatch for lost/unavailable/malicious delegate scenarios — the original motivation for this PR. Session-key self-revocation is purely additive and matches OAuth 2.0 RFC 7009, GitHub PAT, and Power-of-Attorney patterns.


Note: the current code already silently clears an invalid session_key_sig on the revocation path (line 113 in the handler), so the only gap is that a session-key-only revocation (no user_sig) is not supported at all. The change to support it would be small and additive.

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.

3 participants