Skip to content

feat: delegations can require calls to carry a signed sender_info#10452

Closed
aterga wants to merge 2 commits into
masterfrom
claude/fervent-mccarthy-tmfzpw-sender-info-required
Closed

feat: delegations can require calls to carry a signed sender_info#10452
aterga wants to merge 2 commits into
masterfrom
claude/fervent-mccarthy-tmfzpw-sender-info-required

Conversation

@aterga

@aterga aterga commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Delegations can require calls to carry a signed sender_info

This is step 1 of combining the two read-only-delegation experiments (#10447: sender_info attribute checking, #10449: delegation-level permissions): request delegations gain one bit — an optional sender_info_required field, covered by the delegation signature — that forces update calls authenticated through the chain to carry a (signed and verified) sender_info.

Why

The sender_info attribute mechanism of #10447 enforces implicit:permissions = "queries" when the attributes are present — but the party we want to constrain (the dapp holding the session key) decides whether to attach them, so it can lift the restriction by omitting the blob. This bit closes that hole while keeping all permission semantics in the certified attributes:

  • The delegation (which the dapp cannot alter — the field is part of the representation-independent hash, so stripping it invalidates the signature) says: "every update call must present the certified session attributes."
  • The attributes (sender_info, signed by the same issuer under ic-sender-info) say what the session may do, e.g. implicit:permissions = "queries" → update calls rejected per feat: reject update calls when sender_info restricts permissions to queries #10447.

Together: an issuer like Internet Identity signs a read-only delegation chain whose update calls must carry attributes that in turn forbid update calls — protocol-enforced, regardless of client cooperation. #10447 is being rebased on top of this PR so the stack forms the complete combined design.

Semantics

  • Field absent or false → today's behavior, byte-for-byte identical hashes for existing delegations; nothing in the ecosystem changes.
  • sender_info_required = true anywhere in the chain → update calls (/call path) without a sender_info are rejected with the new RequestValidationError::SenderInfoRequiredByDelegation; calls with one proceed through the existing signature verification of the blob. Requirements only accumulate along the chain (like targets).
  • Query and read_state requests are unaffected: read_state envelopes cannot carry a sender_info at all, and queries are not restricted by the permissions attribute anyway.
  • The presence check runs after envelope/delegation signature validation but before the (expensive) canister-signature verification of the sender_info itself.
  • Backward compatibility mirrors feat: reject update calls for delegations with queries-only permissions #10449's analysis: old replicas recompute the delegation hash without the unknown field, so restricted delegations fail closed on un-upgraded replicas (unusable rather than unrestricted); rollout is replicas first, issuers after.

Changes

  • ic-types: Delegation.sender_info_required (optional bool; hashed as nat 0/1 under the key sender_info_required), constructor, accessor; CBOR encoding tests.
  • ic-validator: DelegationRestrictions threaded through delegation-chain validation; presence enforcement on the call path; new error variant mirrored in ic-validator-ingress-message.
  • ic-validator-http-request-test-utils: delegate_to_with_sender_info_required chain-builder support.
  • Tests: signed-bytes golden test; e2e tests covering update-without-sender_info rejected / update-with-valid-sender_info accepted (chain rooted at the same canister signer that signed the attributes, mirroring the II setup) / query unaffected / false unrestricted / requirement in the middle of a chain.

Follow-ups

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

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz


Generated by Claude Code

Adds an optional sender_info_required field to request delegations,
covered by the delegation signature. When a delegation in a request's
delegation chain sets it to true, update calls authenticated through
that chain must carry a (signed and verified) sender_info; calls
without one are rejected with the new SenderInfoRequiredByDelegation
error. Query and read_state requests are unaffected (read_state cannot
even carry a sender_info, and query responses cannot change state).

Since the field is part of the delegation's representation-independent
hash, the bearer of the delegation cannot lift the requirement by
stripping the field: doing so invalidates the delegation signature. For
the same reason, replicas that predate this change fail closed on such
delegations.

Combined with sender_info attribute checking (#10447), this lets
issuers like Internet Identity enforce read-only sessions end to end:
the delegation forces the certified session attributes to be present on
every update call, and the attributes' implicit:permissions = "queries"
then causes the call to be rejected. Without this bit, a dapp holding
the session key could lift the restriction by omitting the sender_info.

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 introduces an optional sender_info_required bit on request delegations (covered by the delegation signature / representation-independent hash) and enforces that update calls authenticated via a delegation chain must include a sender_info when any delegation in the chain requires it. This is part of combining the read-only delegation experiments by making “certified attributes must be present” protocol-enforceable.

Changes:

  • Add Delegation.sender_info_required: Option<bool> (hashes as nat 0/1 under sender_info_required) plus constructor/accessor and CBOR coverage.
  • Thread DelegationRestrictions through delegation validation and enforce sender_info presence on the update /call path (with a new SenderInfoRequiredByDelegation error).
  • Add test utilities and ingress-message e2e tests covering required/optional behavior and chain accumulation.

Reviewed changes

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

Show a summary per file
File Description
rs/validator/src/ingress_validation.rs Introduces DelegationRestrictions, propagates sender_info_required through delegation validation, and enforces sender_info presence for update calls.
rs/types/types/src/messages/http.rs Adds sender_info_required to Delegation, constructor/accessor, and includes it in signed-bytes hashing when present.
rs/types/types/src/messages/http/tests.rs Updates Delegation CBOR serialization tests to include sender_info_required (null/bool).
rs/validator/tests/ingress_validation.rs Adds a signed-bytes golden test for delegations with sender_info_required.
rs/validator/src/ingress_validation/tests.rs Updates signature-validation unit tests to assert the returned restrictions include sender_info_required == false by default.
rs/validator/http_request_test_utils/src/lib.rs Extends delegation-chain builders to create delegations with sender_info_required.
rs/validator/ingress_message/tests/validate_request.rs Adds e2e tests for “required ⇒ reject missing sender_info”, “required ⇒ accept valid sender_info”, and “query unaffected”.
rs/validator/ingress_message/src/lib.rs Exposes the new error variant in the ingress-message wrapper crate and its Display text.
rs/validator/ingress_message/src/internal/mod.rs Maps the new ic_validator error variant into the wrapper crate’s error enum.

💡 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/src/ingress_validation.rs
Avoid duplicating the shared request-content validation on the call
path by enforcing the sender_info_required restriction inside
validate_request_content behind a parameter, and fix a comment to say
query requests rather than query responses.

https://claude.ai/code/session_01BQNgPJKgxWohrJ6owwJBzz

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 makes the sender_info-presence bit unnecessary for the read-only-sessions POC.


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