Skip to content

feat: reject update calls for delegations with queries-only permissions#10449

Draft
aterga wants to merge 4 commits into
masterfrom
claude/fervent-mccarthy-tmfzpw-delegation-permissions
Draft

feat: reject update calls for delegations with queries-only permissions#10449
aterga wants to merge 4 commits into
masterfrom
claude/fervent-mccarthy-tmfzpw-delegation-permissions

Conversation

@aterga

@aterga aterga commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Queries-only permissions on delegations

This is now the consolidated read-only-sessions POC (the previously stacked alternatives #10447/#10452, which carried the restriction in certified sender_info attributes, were closed in favor of this more lightweight approach).

What it does

Request delegations gain an optional permissions field, mirroring the existing targets field. Supported values:

  • permissions absent or "all" → the sender can execute all functions — queries, replicated queries, and updates (today's semantics; byte-for-byte identical hash for existing delegations without the field).
  • "queries" → the sender can only execute queries: requests to /call endpoints (updates and replicated queries) authenticated through such a chain are rejected during ingress validation with the new RequestValidationError::UpdateCallNotPermittedByDelegation; query and read_state requests remain permitted.
  • Any other value → the delegation is rejected as AuthenticationError::UnsupportedDelegationPermissions (fail-closed: no pre-existing delegations carry the field, so unknown values need no legacy leniency).

A restriction anywhere in the chain applies to the whole chain (like targets, restrictions only accumulate).

Why this can't be bypassed, and why it's backward compatible

  • The field is part of the delegation's representation-independent hash, i.e. covered by the delegation signature. A dapp holding the session key cannot strip the field — doing so changes the hash and invalidates the signature.
  • Old replicas fail closed: they parse the delegation without the unknown field (serde ignores it), recompute the hash without it, and signature verification fails. A restricted delegation is unusable on un-upgraded replicas rather than silently unrestricted. Rollout: replicas first, issuers (Internet Identity) after.
  • Delegations without the field are completely unaffected, so existing agents, dapps, and issuers need no changes. Agents only need to round-trip the new field once they want to carry restricted delegations; an agent that drops it fails closed.
  • The enforcement point is HttpRequestVerifier<SignedIngressContent> — the same verifier used by the ingress manager for block validation, so a malicious boundary node or replica cannot smuggle a restricted update call into a block.

Rationale

Enables issuers like Internet Identity to sign read-only delegations: sessions that can read on the user's behalf (query calls) but cannot change state (update calls), enforced by the protocol regardless of the client's cooperation. II sets this field from a "Read-only mode" checkbox in the authorize flow (II-side changes prepared separately).

Historical precedent: the interface spec already extended the delegation map with an optional restriction field once — the senders field (added Dec 2021, never implemented by the replica, removed in interface-spec#246). This PR follows the same formal pattern (verify_delegations accumulation), but implementation-first.

Changes

  • ic-types: Delegation.permissions (optional), new_with_permissions, accessor, inclusion in the signed bytes (hash_of_map key "permissions").
  • ic-validator: DelegationRestrictions (targets + queries-only) threaded through delegation-chain validation; update-call rejection; fail-closed handling of unsupported values; new error variants mirrored in ic-validator-ingress-message.
  • ic-validator-http-request-test-utils: delegate_to_with_permissions chain-builder support.
  • Tests: signed-bytes golden test for the new field; CBOR encoding tests; e2e tests covering update-rejected / query-and-read_state-permitted / "all"-permitted / unsupported-values-rejected (incl. "updates" and the empty string) / restriction-in-the-middle-of-a-chain.

Caveats / follow-ups

  • Calls to query methods submitted via the /call path (replicated queries) are rejected for "queries" delegations by design — the "all" vocabulary makes this explicit.
  • Interface-spec update (delegation map field + verify_delegations formal conditions) prepared for dfinity/portal; II-side issuance and agent (@icp-sdk/core) round-tripping of the field prepared/pending.
  • A system test in rs/tests/crypto/ingress_verification_test.rs (mirroring the delegation tests there) is a natural follow-up; the validator-level e2e tests in this PR cover the enforcement semantics.

cargo test -p ic-types -p ic-validator -p ic-validator-ingress-message passes and clippy is clean; downstream consumers (ic-http-endpoints-public, ic-ingress-manager, ic-canister-client, ic-state-machine-tests) compile. CI is fully green.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz

Adds an optional permissions field to request delegations, mirroring the
existing targets field. When a delegation in a request's delegation
chain carries permissions = "queries", the ingress validator rejects
update calls authenticated through that chain with the new
UpdateCallNotPermittedByDelegation error, while query and read_state
requests remain permitted. Delegations with permissions = "updates" or
without the field are unrestricted, and any other value is rejected as
unsupported (fail-closed, since no pre-existing delegations carry the
field).

The field is covered by the delegation signature (it is part of the
representation-independent hash), so a client cannot strip it to lift
the restriction: removing the field changes the delegation hash and
invalidates the signature. For the same reason, replicas that predate
this change fail closed on restricted delegations.

This enables issuers like Internet Identity to sign read-only
delegations that can read on the user's behalf (perform query calls)
but cannot change state (perform update calls), enforced by the
protocol regardless of the client's cooperation. It is the
strictly-enforced counterpart to the sender_info-based attribute
approach (#10447).

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz

Copilot AI 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.

Pull request overview

This PR extends IC request delegations with an optional permissions field to support queries-only delegations that are enforced during ingress validation (rejecting /call update requests), while remaining backward compatible for existing delegations and failing closed on unsupported permission values.

Changes:

  • Add Delegation.permissions to ic-types, include it in the delegation’s signed bytes, and expose constructors/accessors.
  • Thread delegation permission restrictions through ic-validator delegation-chain validation and reject update calls when any delegation in the chain has permissions = "queries".
  • Add test utilities and end-to-end tests covering acceptance/rejection behavior across request types and invalid permission values.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
rs/validator/tests/ingress_validation.rs Adds a golden signed-bytes test covering the new permissions field hashing.
rs/validator/src/ingress_validation/tests.rs Updates unit tests to match the new validate_signature return type (restrictions vs targets).
rs/validator/src/ingress_validation.rs Implements delegation permissions parsing, restriction accumulation, and update-call rejection.
rs/validator/ingress_message/tests/validate_request.rs Adds e2e tests verifying update rejection and query/read_state acceptance for queries-only delegations.
rs/validator/ingress_message/src/lib.rs Exposes the new validation/authentication error variants in the standalone validator API.
rs/validator/ingress_message/src/internal/mod.rs Maps new ic_validator error variants into ic_validator_ingress_message equivalents.
rs/validator/http_request_test_utils/src/lib.rs Extends delegation-chain builder utilities to create delegations with permissions.
rs/types/types/src/messages/http/tests.rs Updates CBOR serialization tests to include the new permissions field (null or string).
rs/types/types/src/messages/http.rs Adds Delegation.permissions, constructor/accessor, and includes it in signed-bytes hashing when present.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/validator/src/ingress_validation/tests.rs Outdated
Comment thread rs/validator/src/ingress_validation/tests.rs Outdated
Comment thread rs/validator/ingress_message/src/lib.rs
Comment thread rs/validator/src/ingress_validation.rs Outdated
claude added 2 commits June 11, 2026 21:05
Short-circuit the queries-only delegation rejection before sender_info
canister signature verification on the call path, make the error
message byte-identical across the validator crates, and strengthen
test assertions to also check queries_only.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
The supported values of a delegation's permissions field are now:
- "queries": the sender can only execute queries; requests to /call
  endpoints (updates and replicated queries) are rejected.
- "all": the sender can execute all functions (queries, replicated
  queries, and updates); same as an absent permissions field.

Any other value, including the previous "updates", is rejected as
unsupported.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
use ic_validator_ingress_message::AuthenticationError::UnsupportedDelegationPermissions;

#[test]
fn should_reject_update_call_when_delegation_restricts_to_queries() {

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.

Maybe we could similarly extend system tests around ingress validation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants