feat: reject update calls for delegations with queries-only permissions#10449
Conversation
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
There was a problem hiding this comment.
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.permissionstoic-types, include it in the delegation’s signed bytes, and expose constructors/accessors. - Thread delegation permission restrictions through
ic-validatordelegation-chain validation and reject update calls when any delegation in the chain haspermissions = "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.
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
|
✅ No security or compliance issues detected. Reviewed everything up to 937ddbe. Security Overview
Detected Code Changes
|
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
- 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
pierugo-dfinity
left a comment
There was a problem hiding this comment.
Mostly me ranting about Claude's comments
…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
|
Thanks @pierugo-dfinity — addressed all of these in 0ecc183:
Generated by Claude Code |
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
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_infoattributes, were closed in favor of this more lightweight approach).What it does
Request delegations gain an optional
permissionsfield, mirroring the existingtargetsfield. It is modeled as a typed enumDelegationPermissionswhose variants are encoded on the wire (and in the representation-independent hash) as text:permissionsabsent 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/callendpoints (updates and replicated queries) authenticated through such a chain are rejected during ingress validation with the newRequestValidationError::UpdateCallNotPermittedByDelegation; query andread_staterequests remain permitted.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 unionQueries | All | Unrestricted, which this enum mirrors exactly.Why this can't be bypassed, and why it's backward compatible
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
sendersfield (added Dec 2021, never implemented by the replica, removed in interface-spec#246). This PR follows the same formal pattern (verify_delegationsaccumulation), but implementation-first.Changes
ic-types: typedDelegationPermissions { Queries, All }enum (serialized as"queries"/"all"); optionalDelegation.permissionsfield included in the signed bytes (hash_of_mapkey"permissions"); chainablewith_targets/with_permissionsbuilder methods (replacing the formernew_with_targets/new_with_permissionsconstructors so a delegation can carry both).ic-validator:DelegationRestrictions(targets + queries-only) threaded through delegation-chain validation; update-call rejection viaRequestValidationError::UpdateCallNotPermittedByDelegation. The/callpath uses the sharedvalidate_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_permissionschain-builder support.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_permissionsinrs/tests/crypto/ingress_verification_test.rs, taggedlong_test) exercising the full HTTP path against a real replica.Caveats / follow-ups
/callpath (replicated queries) are rejected for"queries"delegations by design — the"all"vocabulary makes this explicit.@icp-sdk/core) round-tripping of the field prepared/pending.cargo test -p ic-types -p ic-validator -p ic-validator-ingress-messagepasses 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