Skip to content

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

Merged
aterga merged 12 commits into
masterfrom
claude/fervent-mccarthy-tmfzpw-delegation-permissions
Jun 24, 2026
Merged

feat: reject update calls for delegations with queries-only permissions#10449
aterga merged 12 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 the consolidated read-only-sessions feature (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. It is modeled as a typed enum DelegationPermissions whose variants are encoded on the wire (and in the representation-independent hash) as text:

  • permissions absent or "all" (DelegationPermissions::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" (DelegationPermissions::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 → because the field is a closed enum, an unsupported value fails to deserialize, so the request is rejected when it is decoded (before validation). There is no dedicated validator error for it.

A restriction anywhere in the chain applies to the whole chain (like targets, restrictions only accumulate). The matching interface-spec change (dfinity/developer-docs#292) models the field as the union Queries | All | Unrestricted, which this enum mirrors exactly.

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 unknown map keys), 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: typed DelegationPermissions { Queries, All } enum (serialized as "queries"/"all"); optional Delegation.permissions field included in the signed bytes (hash_of_map key "permissions"); chainable with_targets/with_permissions builder methods (replacing the former new_with_targets/new_with_permissions constructors so a delegation can carry both).
  • ic-validator: DelegationRestrictions (targets + queries-only) threaded through delegation-chain validation; update-call rejection via RequestValidationError::UpdateCallNotPermittedByDelegation. The /call path uses the shared validate_request_content (no special-casing) and checks the restriction afterwards.
  • ic-validator-ingress-message: the new error variant mirrored.
  • ic-validator-http-request-test-utils: delegate_to_with_permissions chain-builder support.
  • Tests: signed-bytes golden test for the new field; CBOR encoding + round-trip tests; a decode-rejection test (delegation_permissions_rejects_unsupported_value) covering unsupported values including case/whitespace variants; validator-level e2e tests (update-rejected / query-and-read_state-permitted / "all"-permitted / restriction-mid-chain / combined targets+permissions); and a system test (requests_with_delegation_permissions in rs/tests/crypto/ingress_verification_test.rs, tagged long_test) exercising the full HTTP path against a real replica.

Caveats / follow-ups

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) and both affected system-test binaries compile.

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
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
Comment thread rs/validator/ingress_message/tests/validate_request.rs
@aterga aterga marked this pull request as ready for review June 23, 2026 10:36
@aterga aterga requested a review from a team as a code owner June 23, 2026 10:36
@zeropath-ai

zeropath-ai Bot commented Jun 23, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 937ddbe.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/tests/consensus/request_auth_malicious_replica_test.rs
    Update Delegation::new_with_targets to Delegation::new().with_targets()
► rs/tests/crypto/ingress_verification_test.rs
    Add test for delegation permissions
► rs/types/types/src/messages.rs
    Add DelegationPermissions enum
► rs/types/types/src/messages/http.rs
    Implement DelegationPermissions enum and add to Delegation struct
    Add with_permissions method to Delegation
    Add tests for DelegationPermissions CBOR serialization and deserialization
    Add tests for DelegationPermissions rejecting unsupported values
► rs/validator/http_request_test_utils/src/lib.rs
    Add delegate_to_with_permissions method to DirectAuthenticationScheme
    Add delegate_to_with_permissions method to DelegationChainBuilder
► rs/validator/ingress_message/src/internal/mod.rs
    Map RequestValidationError::UpdateCallNotPermittedByDelegation
► rs/validator/ingress_message/src/lib.rs
    Add RequestValidationError::UpdateCallNotPermittedByDelegation
    Update HttpRequestVerifier trait and implementations to handle delegation permissions
► rs/validator/ingress_message/tests/validate_request.rs
    Add tests for delegation permissions
► rs/validator/src/ingress_validation.rs
    Implement DelegationRestrictions struct
    Update validate_signature and validate_delegations to handle delegation permissions
    Add UpdateCallNotPermittedByDelegation error
► rs/validator/src/ingress_validation/tests.rs
    Add tests for delegation permissions
Refactor ► rs/types/types/src/messages/http.rs
    Simplify Delegation::new_with_targets to Delegation::new().with_targets()

Comment thread rs/types/types/src/messages/http.rs Outdated
Adds the test coverage identified as missing:

1. System test (rs/tests/crypto/ingress_verification_test.rs):
   requests_with_delegation_permissions exercises the full HTTP path
   against a real replica — "queries" rejects update calls (across all
   /call API versions) while permitting query and read_state, "all" is
   accepted, and unsupported values (incl. "updates") are rejected for
   all request kinds; covers the whole-chain semantics.

2. CBOR wire round-trip (ic-types http tests): confirms the permissions
   field survives serialize/deserialize and that a delegation encoded
   without the field deserializes to None (backward compatibility).

3. Combined targets + permissions CBOR encoding case.

4. Lower-level validate_delegations unit tests (ic-validator) asserting
   queries_only is set for "queries", unset for "all"/absent, and that
   unsupported values (incl. "updates") yield UnsupportedDelegationPermissions.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Comment thread rs/validator/ingress_message/tests/validate_request.rs Outdated
Comment thread rs/validator/ingress_message/src/lib.rs Outdated
Comment thread rs/validator/src/ingress_validation/tests.rs Outdated
Comment thread rs/validator/src/ingress_validation.rs
Comment thread rs/validator/ingress_message/tests/validate_request.rs Outdated
Comment thread rs/tests/crypto/ingress_verification_test.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated
Comment thread rs/validator/src/ingress_validation/tests.rs Outdated
Comment thread rs/validator/src/ingress_validation.rs
Comment thread rs/validator/src/ingress_validation.rs
claude added 2 commits June 23, 2026 13:00
- Replace Delegation::new_with_targets/new_with_permissions with chainable
  with_targets/with_permissions builder methods, so a delegation can carry
  both targets and permissions.
- Remove the call-path duplication of validate_request_content: the
  queries-only rejection now happens after the shared validation (the
  earlier short-circuit-before-sender_info did not actually prevent DoS,
  since the query/read_state endpoints validate sender_info too).
- Clarify docs: an update call or replicated query submitted to /call is
  rejected by a queries-only delegation.
- Tests: add case/whitespace unsupported-permission variants; add an e2e
  test combining delegate_to_with_targets and delegate_to_with_permissions
  in one chain; reword the misleading "previously-used" comment.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Per the settled interface-spec model (dfinity/developer-docs#292, which
defines the field as the union `Queries | All | Unrestricted`), replace
the `Option<String>` `permissions` field with a typed
`Option<DelegationPermissions>` enum whose `Queries`/`All` variants are
encoded on the wire (and in the representation-independent hash) as the
text `"queries"`/"all"".

Consequences:
- Unsupported values are now rejected when the request is decoded (the
  field is a closed enum), so the runtime
  AuthenticationError::UnsupportedDelegationPermissions check in
  validate_delegations is gone, along with the error variant in both the
  validator and ingress-message crates.
- The "unsupported value rejected" coverage moves to a CBOR-decoding test
  in ic-types (delegation_permissions_rejects_unsupported_value), which
  also covers case/whitespace variants of the supported values.
- The validator/e2e/system tests that previously constructed invalid
  string permissions are removed or switched to the typed enum.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Comment thread rs/validator/src/ingress_validation/tests.rs Outdated

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

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread rs/validator/src/ingress_validation.rs
Comment thread rs/types/types/src/messages/http.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated

@pierugo-dfinity pierugo-dfinity 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.

Mostly me ranting about Claude's comments

Comment thread rs/tests/crypto/ingress_verification_test.rs Outdated
Comment thread rs/tests/crypto/ingress_verification_test.rs Outdated
Comment thread rs/tests/crypto/ingress_verification_test.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated
Comment thread rs/types/types/src/messages/http.rs Outdated
@aterga aterga enabled auto-merge June 24, 2026 08:38
…ents

Trim verbose doc comments on DelegationPermissions and the Delegation
builders, drop the broken interface-spec anchor, and clarify the
system-test comments per reviewer feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz

aterga commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @pierugo-dfinity — addressed all of these in 0ecc183:

  • Trimmed the verbose doc comments on DelegationPermissions, with_targets, and with_permissions to the concise one-liners you suggested.
  • Dropped the broken #authentication anchor (the link now points at the spec page).
  • Removed the "Unsupported values…" comment in the system test (artifact of an earlier refactor, no longer relevant — unsupported values are a closed-enum decode concern covered elsewhere).
  • Rewrote the confusing "Both remaining outcomes…" comments to "Query calls / read_state requests are permitted regardless of the outcome."

Generated by Claude Code

Comment thread rs/types/types/src/messages/http.rs Outdated
Point the doc comment at the post-migration URL with a valid
`#authentication` anchor, per reviewer feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
@aterga aterga added this pull request to the merge queue Jun 24, 2026
Merged via the queue into master with commit 9c15c2d Jun 24, 2026
37 checks passed
@aterga aterga deleted the claude/fervent-mccarthy-tmfzpw-delegation-permissions branch June 24, 2026 10:50
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.

6 participants