Skip to content

feat: reject update calls when sender_info restricts permissions to queries#10447

Closed
aterga wants to merge 4 commits into
claude/fervent-mccarthy-tmfzpw-sender-info-requiredfrom
claude/fervent-mccarthy-tmfzpw
Closed

feat: reject update calls when sender_info restricts permissions to queries#10447
aterga wants to merge 4 commits into
claude/fervent-mccarthy-tmfzpw-sender-info-requiredfrom
claude/fervent-mccarthy-tmfzpw

Conversation

@aterga

@aterga aterga commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Queries-only permission for sender_info — combined read-only-session design

Stacked on #10452 (delegations can require a signed sender_info): this PR is now rebased on top of that branch, and together they form the complete, protocol-enforced read-only-session design. The base of this PR is set to the #10452 branch so the diff shows only the attribute-checking part; GitHub will retarget it to master automatically when #10452 merges.

The combined design

  1. feat: delegations can require calls to carry a signed sender_info #10452 — the delegation bit: the issuer (Internet Identity) signs the session delegation with sender_info_required = true. The field is covered by the delegation signature, so the dapp holding the session key cannot strip it; update calls without a sender_info are rejected (SenderInfoRequiredByDelegation).
  2. This PR — the attribute semantics: when an update call carries a sender_info whose info blob is a Candid-encoded ICRC-3 map (the format II certifies under the ic-sender-info domain separator) with the attribute implicit:permissions = "queries", the call is rejected (UpdateCallNotPermittedBySenderInfo). Query calls are unaffected.

Together: a read-only session must present its certified attributes on every update call (1), and those attributes forbid update calls (2) — so the restriction holds regardless of client cooperation, while all permission semantics stay in the extensible attribute map rather than the delegation format. The new read_only_sessions e2e test module in rs/validator/ingress_message/tests/validate_request.rs demonstrates the full story: update with queries-only attributes → rejected; update omitting attributes → rejected; queries → permitted; update with "updates" attributes → permitted.

Design notes (attribute checking, this PR)

  • Enforcement point: HttpRequestVerifier<SignedIngressContent>::validate_request — the same layer that enforces delegation targets; the ingress manager uses the same verifier for payload validation, so the restriction also holds during block validation.
  • Attribute key: implicit:permissions (single colon), following II's existing implicit:nonce / implicit:origin convention. The attribute parsing only runs after the canister-signature verification of the blob succeeded, with bounded Candid decoding quotas.
  • Fail-open for unknown values (legacy-blob compatibility): only the exact value "queries" restricts. Note the presence requirement of feat: delegations can require calls to carry a signed sender_info #10452 and the delegation field itself are fail-closed.
  • Update-call path, not update methods: replicated queries via /call are also rejected (the validator cannot know method kinds); restricted callers use the query path.
  • The ICRC-3 Value type is mirrored minimally inside ic-validator to avoid pulling ledger packages into the consensus-critical validator.

Tests

  • Unit tests for permission extraction/decision logic.
  • End-to-end tests with validly canister-signed sender_info, plus the combined read_only_sessions module described above.
  • System test (requests_with_sender_info_permissions in rs/tests/crypto/ingress_verification_test.rs) demonstrating attribute enforcement against a real replica across all /call API versions.

Related

cargo test -p ic-validator -p ic-validator-ingress-message -p ic-types passes on the stack, clippy is clean, and the system test binary compiles. Bazel was not available in the development container, so CI should confirm the Bazel build.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz

Comment thread rs/validator/ingress_message/tests/validate_request.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

This PR introduces a protocol-level restriction on sender_info for update calls: if a canister-signed sender_info.info decodes as an ICRC-3 map with implicit:permissions = "queries", the replica rejects /call requests during ingress validation (while leaving query/read_state validation behavior unchanged).

Changes:

  • Add Candid-decoding of sender_info.info in ic-validator to extract implicit:permissions and reject update calls when it equals "queries".
  • Introduce a new validation error UpdateCallNotPermittedBySenderInfo and plumb it through ic-validator-ingress-message.
  • Add unit + integration tests and update Cargo/Bazel dependencies needed for Candid decoding in the validator and for encoding test payloads.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
rs/validator/src/ingress_validation.rs Add permission extraction/decision logic and new validation error for update calls.
rs/validator/src/ingress_validation/tests.rs Add unit tests covering permission extraction and fail-open behavior.
rs/validator/ingress_message/tests/validate_request.rs Add end-to-end tests verifying update rejection vs query acceptance with permissions.
rs/validator/ingress_message/src/lib.rs Add new public error variant and Display message in the ingress-message wrapper crate.
rs/validator/ingress_message/src/internal/mod.rs Map the new ic_validator error into the wrapper crate error.
rs/validator/ingress_message/Cargo.toml Add candid dev-dependency for encoding test payloads.
rs/validator/ingress_message/BUILD.bazel Add Bazel test dependency on candid.
rs/validator/Cargo.toml Add candid, serde, and serde_bytes dependencies needed for decoding in validator.
rs/validator/BUILD.bazel Add Bazel deps for candid, serde, and serde_bytes to the validator library.
Cargo.lock Record new dependency edges for candid/serde_bytes.

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

Comment thread rs/validator/src/ingress_validation.rs Outdated
Comment thread rs/validator/BUILD.bazel
"//rs/crypto/tree_hash",
"//rs/limits",
"//rs/types/types",
"@crate_index//:candid",

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.

Note to reviewer.

If you dislike adding candid as a dependency here, we can provide a non-candid endpoint in II to serve a different encoding of sender_info - please indicate your preferred encoding to keep this thread actionable.

claude added 4 commits June 12, 2026 10:06
…ueries

When an update call carries a sender_info whose info blob is a
Candid-encoded ICRC-3 map (the format used by Internet Identity to
certify request attributes, signed under the "ic-sender-info" domain
separator) and the map contains the "implicit:permissions" attribute
with the text value "queries", the ingress validator now rejects the
call with the new UpdateCallNotPermittedBySenderInfo error. Query calls
and all other cases (no sender info, unparseable info blob, absent
attribute, or any other attribute value) remain permitted.

This enables Internet Identity users to sign limited delegations that
can only read on their behalf (call query methods) but cannot change
state (call update methods), with the restriction enforced by the
protocol rather than by each individual canister.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Import BTreeMap and CandidType instead of qualifying them in the
sender_info integration tests, and avoid cloning the permissions string
in sender_info_permissions by removing the entry from the owned map.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Extends the ingress verification system test suite with a test that
demonstrates the implicit:permissions sender_info attribute end-to-end:
a signer canister certifies an ICRC-3 attributes map restricting the
sender to "queries", after which the protocol rejects the sender's
update calls during ingress validation while query calls remain
permitted (and the receiving canister can read the certified attributes
via the msg_caller_info system API). Attribute maps with permissions
"updates", or without the permissions attribute, leave update calls
permitted.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
Adds e2e tests for the combined design: a delegation with
sender_info_required = true forces update calls to carry the certified
session attributes, whose implicit:permissions attribute then determines
whether update calls are permitted. Covers rejection with queries-only
attributes, rejection when omitting the attributes, and acceptance of
queries and of update calls with updates-permissions.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz
@aterga aterga force-pushed the claude/fervent-mccarthy-tmfzpw branch from 535eb25 to 0c5ce91 Compare June 12, 2026 10:10
@aterga aterga changed the base branch from master to claude/fervent-mccarthy-tmfzpw-sender-info-required June 12, 2026 10:10

aterga commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of the more lightweight approach in #10449 (delegations carry the queries/all permissions directly), which is fully green and consolidates this POC. The sender_info-attribute checking explored here remains documented in this PR's history should the certified-attributes route be revisited.


Generated by Claude Code

@aterga aterga closed this Jun 12, 2026
pull Bot pushed a commit to mikeyhodl/ic that referenced this pull request Jun 24, 2026
…ns (dfinity#10449)

# Queries-only permissions on delegations

**This is the consolidated read-only-sessions feature** (the previously
stacked alternatives dfinity#10447/dfinity#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](dfinity/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

* 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 change in dfinity/developer-docs#292; II-side issuance
and agent (`@icp-sdk/core`) round-tripping of the field
prepared/pending.

`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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

3 participants