diff --git a/.sources/portal b/.sources/portal index e636b0a1..d1da46dd 160000 --- a/.sources/portal +++ b/.sources/portal @@ -1 +1 @@ -Subproject commit e636b0a1d1d859b584ed6b2cd5b7ff953280ebcc +Subproject commit d1da46dd64fa89dcd191b0b9d901f84b3b2a615a diff --git a/docs/reference/http-gateway-spec.md b/docs/reference/http-gateway-spec.md index 5ef9047d..07a3da09 100644 --- a/docs/reference/http-gateway-spec.md +++ b/docs/reference/http-gateway-spec.md @@ -29,35 +29,18 @@ An HTTP request by an HTTP client is handled by these steps: ## Canister ID Resolution -The HTTP Gateway needs to know the canister ID of the canister to talk to, and obtains that information from the hostname as follows: +The HTTP Gateway needs to determine the canister ID for each incoming request before it can forward the request to the Internet Computer. The mechanism by which the canister ID is resolved is not prescribed by this specification and may vary across implementations. Some examples of how a canister ID can be obtained include: -1. If the hostname is in the following table, use the given canister ids: +- Extracted from the hostname (e.g., a canister ID encoded as a subdomain). +- Looked up via DNS (e.g., a TXT record at a well-known subdomain). +- Retrieved from a static mapping configured in the gateway. +- Provided via an HTTP response header returned during a pre-flight lookup. - | Hostname | Canister id | - | -------------------- | ----------------------------- | - | `identity.ic0.app` | `rdmx6-jaaaa-aaaaa-aaadq-cai` | - | `nns.ic0.app` | `qoctq-giaaa-aaaaa-aaaea-cai` | - | `dscvr.one` | `h5aet-waaaa-aaaab-qaamq-cai` | - | `dscvr.ic0.app` | `h5aet-waaaa-aaaab-qaamq-cai` | - | `personhood.ic0.app` | `g3wsl-eqaaa-aaaan-aaaaa-cai` | +If the HTTP Gateway cannot determine a canister ID for a request, it may handle the request as a standard Web2 request or return an error, depending on the implementation. -2. Check whether the hostname is _raw_ (e.g., `.raw.ic0.app`). If it is the case, fail and handle the request as a Web2 request, otherwise, continue. +## API Boundary Node Resolution -3. Check whether the canister ID is embedded in the hostname by splitting the hostname and finding the first occurrence of a valid canister ID from the right. If there is a canister ID embedded in the hostname, use it. - -4. Check whether the canister is hosted on the IC using a custom domain. There are two options: - - - Check whether there is a TXT record containing a canister ID at the `_canister-id`-subdomain (e.g., to see whether `foo.com` is hosted on the IC, make a DNS lookup for the TXT record of `_canister-id.foo.com`) and use the specified canister ID; - - - Make a `HEAD` request to the hostname. If the response contains an `x-ic-canister-id` header, use the value of this header as the canister ID. - -5. Else fail and handle the request as a Web2 request. - -If the hostname was of the form `.ic0.app`, it is a _safe_ hostname; if it was of the form `.raw.ic0.app`, it is a _raw_ hostname. Note that other domains may also be used to access canisters, such as `icp0.io`. The same logic concerning _raw_ domains can also be applied to these alternative domains. - -## API Gateway Resolution - -An API Gateway forwards Candid encoded HTTP requests to the relevant replica node. Any requests to the Internet Computer made by an HTTP Gateway are forwarded through these API gateways. The hostname of the API gateways is always `icp-api.io`. +An API Boundary Node forwards Candid encoded HTTP requests to the relevant replica node. Any requests to the Internet Computer made by an HTTP Gateway are forwarded through these API boundary nodes. The hostname of the API boundary nodes is always `icp-api.io`. ## HTTP Request Encoding @@ -87,7 +70,7 @@ The full [Candid](https://github.com/dfinity/candid/blob/master/spec/Candid.md) ## Query Calls -The encoded HTTP request is sent as a query call according to the [HTTPS Interface](/references/ic-interface-spec#http-query) via the API Gateway resolved according to [API Gateway Resolution](#api-gateway-resolution). +The encoded HTTP request is sent as a query call according to the [HTTPS Interface](/references/ic-interface-spec#http-query) via the API Boundary Node resolved according to [API Boundary Node Resolution](#api-boundary-node-resolution). ## HTTP Response Decoding diff --git a/docs/reference/ic-interface-spec.md b/docs/reference/ic-interface-spec.md index 4180c8b7..c19d610b 100644 --- a/docs/reference/ic-interface-spec.md +++ b/docs/reference/ic-interface-spec.md @@ -752,14 +752,22 @@ In order to call a canister, the user makes a POST request to `/api/v3/canister/ - `request_type` (`text`): Always `call` -- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). The canister will not start processing a call past its `ingress_expiry`. - - `canister_id` (`blob`): The principal of the canister to call. - `method_name` (`text`): Name of the canister method to call. - `arg` (`blob`): Argument to pass to the canister method. +- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). The canister will not start processing a call past its `ingress_expiry`. + +- `sender_info` (`map`, optional): Map with fields: + + - `info` (`blob`, required): The sender information passed to the canister. + + - `signer` (`blob`, required): The principal of the signing canister. This must be equal to the canister ID encoded in the `sender_pubkey`, i.e. the `signing_canister_id` component of the canister signature public key, as described in [canister signature](#canister-signatures). + + - `sig` (`blob`, required): Signature to authenticate the `info` field. This signature *must* be a [canister signature](#canister-signatures), using the 15 bytes `\x0Eic-sender-info` as the domain separator for the payload, and *must* verify using `sender_pubkey` as the canister signature public key. + The HTTP response to this request can have the following forms: - 200 HTTP status with a non-empty body. This status is returned if the canister call completed within an implementation-specific timeout or was rejected within an implementation-specific timeout. @@ -800,14 +808,22 @@ In order to call a canister, the user makes a POST request to `/api/v2/canister/ - `request_type` (`text`): Always `call` -- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). The canister will not start processing a call past its `ingress_expiry`. - - `canister_id` (`blob`): The principal of the canister to call. - `method_name` (`text`): Name of the canister method to call - `arg` (`blob`): Argument to pass to the canister method +- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). The canister will not start processing a call past its `ingress_expiry`. + +- `sender_info` (`map`, optional): Map with fields: + + - `info` (`blob`, required): The sender information passed to the canister. + + - `signer` (`blob`, required): The principal of the signing canister. This must be equal to the canister ID encoded in the `sender_pubkey`, i.e. the `signing_canister_id` component of the canister signature public key, as described in [canister signature](#canister-signatures). + + - `sig` (`blob`, required): Signature to authenticate the `info` field. This signature *must* be a [canister signature](#canister-signatures), using the 15 bytes `\x0Eic-sender-info` as the domain separator for the payload, and *must* verify using `sender_pubkey` as the canister signature public key. + The HTTP response to this request can have the following responses: - 202 HTTP status with empty body. Implying the request was accepted by the IC for further processing. Users should use [`read_state`](#http-read-state) to determine the status of the call. @@ -961,14 +977,22 @@ In order to make a query call to a canister, the user makes a POST request to `/ - `request_type` (`text`): Always `"query"`. -- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). - - `canister_id` (`blob`): The principal of the canister to call. - `method_name` (`text`): Name of the canister method to call. - `arg` (`blob`): Argument to pass to the canister method. +- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication). + +- `sender_info` (`map`, optional): Map with fields: + + - `info` (`blob`, required): The sender information passed to the canister. + + - `signer` (`blob`, required): The principal of the signing canister. This must be equal to the canister ID encoded in the `sender_pubkey`, i.e. the `signing_canister_id` component of the canister signature public key, as described in [canister signature](#canister-signatures). + + - `sig` (`blob`, required): Signature to authenticate the `info` field. This signature *must* be a [canister signature](#canister-signatures), using the 15 bytes `\x0Eic-sender-info` as the domain separator for the payload, and *must* verify using `sender_pubkey` as the canister signature public key. + The HTTP response to this request can have the following forms: - 200 HTTP status with a non-empty body consisting of a CBOR (see [CBOR](#cbor)) map with the following fields: @@ -1061,6 +1085,8 @@ It must be contained in the canister ranges of a subnet, otherwise the correspon - If the request is a query call to the Management Canister (`aaaaa-aa`), then: + - If the call is to the `list_canisters` method, then any principal can be used as the effective canister id for this call. + - If the `arg` is a Candid-encoded record with a `canister_id` field of type `principal`, then the effective canister id must be that principal. - Otherwise, the call is rejected by the system independently of the effective canister id. @@ -1561,6 +1587,10 @@ defaulting to `I = i32` if the canister declares no memory. ic0.msg_arg_data_copy : (dst : I, offset : I, size : I) -> (); // I U RQ NRQ TQ CQ Ry CRy F ic0.msg_caller_size : () -> I; // * ic0.msg_caller_copy : (dst : I, offset : I, size : I) -> (); // * + ic0.msg_caller_info_data_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F + ic0.msg_caller_info_data_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F + ic0.msg_caller_info_signer_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F + ic0.msg_caller_info_signer_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F ic0.msg_reject_code : () -> i32; // Ry Rt CRy CRt C ic0.msg_reject_msg_size : () -> I ; // Rt CRt ic0.msg_reject_msg_copy : (dst : I, offset : I, size : I) -> (); // Rt CRt @@ -1734,6 +1764,17 @@ The canister can access an argument. For `canister_init`, `canister_post_upgrade The identity of the caller, which may be a canister id or a user id. During canister installation or upgrade, this is the id of the user or canister requesting the installation or upgrade. During a system task (heartbeat or global timer), this is the id of the management canister. +- `ic0.msg_caller_info_data_size : () → I`, `ic0.msg_caller_info_signer_size : () → I` and `ic0.msg_caller_info_data_copy : (dst : I, offset : I, size : I) → ()`; and `ic0.msg_caller_info_signer_copy : (dst : I, offset : I, size : I) → ()`; `I ∈ {i32, i64}` + + Auxiliary information about the caller as provided by the canister with which the caller's identity is associated (i.e., the public key of the canister signature is equal to the public key of the caller's identity). + These functions can only return non-empty values if the caller is a self-authenticating principal authenticated by canister signatures. In particular, they always return empty values when the caller is another canister. + + The `caller_info_data` may include information such as identity attributes of the caller. + The `_signer_` functions return the canister ID of the canister providing the signature, and the `_data_` functions return the data provided by the canister. + This auxiliary information can only be set if the caller principal is derived from the public key corresponding to a canister signature, and it is guaranteed to be properly signed by the issuing canister. + + These functions trap in `canister_init`, `canister_post_upgrade`, `canister_pre_upgrade`, canister http outcall transform, the `(start)` module initialization function, and system tasks (`canister_heartbeat` or `canister_global_timer` or `canister_on_low_wasm_memory`). + - `ic0.msg_reject_code : () → i32` Returns the reject code, if the current function is invoked as a reject callback or as a cleanup callback of a reject callback. @@ -3275,6 +3316,24 @@ Replica-signed queries may improve security because the recipient can verify the ::: +### IC method `list_canisters` {#ic-list_canisters} + +This method can only be called by external users with subnet admin privileges via non-replicated (query) calls, i.e., it cannot be called by canisters, cannot be called via replicated calls, and cannot be called from composite query calls. + +This method returns the list of all canisters on the subnet as consecutive canister ID ranges. Deleted canisters are not included in the result. + +A canister ID range is a record with the following fields: + +- `start` (`principal`): the first canister ID in the range (inclusive); +- `end` (`principal`): the last canister ID in the range (inclusive). + +:::warning + +The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. +Replica-signed queries may improve security because the recipient can verify the response comes from the correct subnet. + +::: + ## The IC Bitcoin API {#ic-bitcoin-api} The Bitcoin API exposed by the management canister is DEPRECATED. @@ -3721,6 +3780,12 @@ mk_self_authenticating_id pk = H(pk) · 0x02 ``` calculates self-authenticating ids. +The function +``` +canister_signature_pk : Principal -> Blob -> PublicKey +``` +calculates the public key of a [canister signature](#canister-signatures). + The function ``` mk_derived_id : Principal -> Blob -> Principal @@ -3768,6 +3833,8 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i Arg = Blob; CallerId = Principal; + CallerInfoData = Blob; + CallerInfoSigner = Blob; Timestamp = Nat; CanisterVersion = Nat; @@ -3853,15 +3920,15 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i new_global_timer : NoGlobalTimer | Nat; cycles_used : Nat; } - update_methods : MethodName ↦ ((Arg, CallerId, Deadline, Env, AvailableCycles) -> UpdateFunc) - query_methods : MethodName ↦ ((Arg, CallerId, Env) -> QueryFunc) - composite_query_methods : MethodName ↦ ((Arg, CallerId, Env) -> CompositeQueryFunc) + update_methods : MethodName ↦ ((Arg, CallerId, CallerInfoData, CallerInfoSigner, Deadline, Env, AvailableCycles) -> UpdateFunc) + query_methods : MethodName ↦ ((Arg, CallerId, CallerInfoData, CallerInfoSigner, Env) -> QueryFunc) + composite_query_methods : MethodName ↦ ((Arg, CallerId, CallerInfoData, CallerInfoSigner, Env) -> CompositeQueryFunc) heartbeat : (Env) -> SystemTaskFunc global_timer : (Env) -> SystemTaskFunc on_low_wasm_memory : (Env) -> SystemTaskFunc - callbacks : (Callback, Response, Deadline, RefundedCycles, Env, AvailableCycles) -> UpdateFunc - composite_callbacks : (Callback, Response, Env) -> UpdateFunc - inspect_message : (MethodName, WasmState, Arg, CallerId, Env) -> Trap | Return { + callbacks : (Callback, CallerId, CallerInfoData, CallerInfoSigner, Response, Deadline, RefundedCycles, Env, AvailableCycles) -> UpdateFunc + composite_callbacks : (Callback, CallerId, CallerInfoData, CallerInfoSigner, Response, Env) -> UpdateFunc + inspect_message : (MethodName, WasmState, Arg, CallerId, CallerInfoData, CallerInfoSigner, Env) -> Trap | Return { status : Accept | Reject; } } @@ -3892,6 +3959,11 @@ Request = { nonce : Blob; ingress_expiry : Nat; sender : UserId; + sender_info : { + info : Blob; + signer : Blob; + sig : Blob; + }; canister_id : CanisterId; method_name : Text; arg : Blob; @@ -3932,6 +4004,8 @@ Message = CallMessage { origin : CallOrigin; caller : Principal; + caller_info_data : Blob; + caller_info_signer : Blob; callee : CanisterId; method_name : Text; arg : Blob; @@ -3940,6 +4014,9 @@ Message } | FuncMessage { call_context : CallId; + caller : Principal; + caller_info_data : Blob; + caller_info_signer : Blob; receiver : CanisterId; entry_point : EntryPoint; queue : Queue; @@ -3973,6 +4050,11 @@ APIReadRequest nonce : Blob; ingress_expiry : Nat; sender : UserId; + sender_info : { + info : Blob; + signer : Blob; + sig : Blob; + }; canister_id : CanisterId; method_name : Text; arg : Blob; @@ -4354,6 +4436,7 @@ A `Request` has an effective canister id according to the rules in [Effective ca ``` is_effective_canister_id(Request {canister_id = ic_principal, method = create_canister, …}, p) is_effective_canister_id(Request {canister_id = ic_principal, method = provisional_create_canister_with_cycles, …}, p) +is_effective_canister_id(CanisterQuery {canister_id = ic_principal, method = list_canisters, …}, p) is_effective_canister_id(Request {canister_id = ic_principal, method = install_chunked_code, arg = candid({target_canister = p, …}), …}, p) is_effective_canister_id(Request {canister_id = ic_principal, arg = candid({canister_id = p, …}), …}, p) is_effective_canister_id(Request {canister_id = p, …}, p), if p ≠ ic_principal @@ -4390,6 +4473,22 @@ Conditions ```html E.content.canister_id ∈ verify_envelope(E, E.content.sender, S.system_time) +if E.sender_pubkey = canister_signature_pk Signing_canister_id Seed: + if not (E.content.sender_info = null): + verify_signature E.sender_pubkey E.content.sender_info.sig ("\x0Eic-sender-info" · E.content.sender_info.info) + E.content.sender_info.signer = Signing_canister_id +else: + E.content.sender_info = null +if E.content.sender = mk_self_authenticating_id (canister_signature_pk Signing_canister_id Seed): + if E.content.sender_info = null: + Caller_info_data = "" + Caller_info_signer = "" + else: + Caller_info_data = E.content.sender_info.info + Caller_info_signer = Signing_canister_id +else: + Caller_info_data = "" + Caller_info_signer = "" |E.content.nonce| <= 32 E.content ∉ dom(S.requests) S.system_time <= E.content.ingress_expiry @@ -4444,7 +4543,7 @@ liquid_balance(S, E.content.canister_id) ≥ 0 canister_version = S.canister_version[E.content.canister_id]; } S.canisters[E.content.canister_id].module.inspect_message - (E.content.method_name, S.canisters[E.content.canister_id].wasm_state, E.content.arg, E.content.sender, Env) = Return {status = Accept;} + (E.content.method_name, S.canisters[E.content.canister_id].wasm_state, E.content.arg, E.content.sender, Caller_info_data, Caller_info_signer, Env) = Return {status = Accept;} ) ``` @@ -4502,6 +4601,17 @@ S.requests[R] = (Received, ECID) S.system_time <= R.ingress_expiry C = S.canisters[R.canister_id] +if R.sender = mk_self_authenticating_id (canister_signature_pk Signing_canister_id Seed): + if R.sender_info = null: + Caller_info_data = "" + Caller_info_signer = "" + else: + Caller_info_data = R.sender_info.info + Caller_info_signer = Signing_canister_id +else: + Caller_info_data = "" + Caller_info_signer = "" + ``` State after @@ -4514,6 +4624,8 @@ S with CallMessage { origin = FromUser { request = R }; caller = R.sender; + caller_info_data = Caller_info_data; + caller_info_signer = Caller_info_signer; callee = R.canister_id; method_name = R.method_name; arg = R.arg; @@ -4611,6 +4723,9 @@ S with Older_messages · FuncMessage { call_context = Ctxt_id; + caller = CM.caller; + caller_info_data = CM.caller_info_data; + caller_info_signer = CM.caller_info_signer; receiver = CM.callee; entry_point = PublicMethod CM.method_name CM.caller CM.arg; queue = CM.queue; @@ -4650,6 +4765,9 @@ S with messages = FuncMessage { call_context = Ctxt_id; + caller = ic_principal; + caller_info_data = ""; + caller_info_signer = ""; receiver = C; entry_point = Heartbeat; queue = Queue { from = System; to = C }; @@ -4691,6 +4809,9 @@ S with messages = FuncMessage { call_context = Ctxt_id; + caller = ic_principal; + caller_info_data = ""; + caller_info_signer = ""; receiver = C; entry_point = GlobalTimer; queue = Queue { from = System; to = C }; @@ -4732,6 +4853,9 @@ S with messages = FuncMessage { call_context = Ctxt_id; + caller = ic_principal; + caller_info_data = ""; + caller_info_signer = ""; receiver = C; entry_point = OnLowWasmMemory; queue = Queue { from = System; to = C }; @@ -4829,19 +4953,19 @@ Env = { Available = Ctxt.available_cycles ( M.entry_point = PublicMethod Name Caller Arg - F = Mod.update_methods[Name](Arg, Caller, Deadline, Env, Available) + F = Mod.update_methods[Name](Arg, M.caller, M.caller_info_data, M.caller_info_signer, Deadline, Env, Available) New_canister_version = S.canister_version[M.receiver] + 1 Wasm_memory_limit = S.wasm_memory_limit[M.receiver] ) or ( M.entry_point = PublicMethod Name Caller Arg - F = query_as_update(Mod.query_methods[Name], Arg, Caller, Env, Available) + F = query_as_update(Mod.query_methods[Name], Arg, M.caller, M.caller_info_data, M.caller_info_signer, Env, Available) New_canister_version = S.canister_version[M.receiver] Wasm_memory_limit = 0 ) or ( M.entry_point = Callback Callback Response RefundedCycles - F = Mod.callbacks(Callback, Response, Deadline, RefundedCycles, Env, Available) + F = Mod.callbacks(Callback, M.caller, M.caller_info_data, M.caller_info_signer, Response, Deadline, RefundedCycles, Env, Available) New_canister_version = S.canister_version[M.receiver] + 1 Wasm_memory_limit = 0 ) @@ -4921,6 +5045,8 @@ then }; caller = M.receiver; + caller_info_data = ""; + caller_info_signer = ""; callee = call.callee; method_name = call.method_name; arg = call.arg; @@ -4990,8 +5116,8 @@ validate_sender_canister_version(new_calls, canister_version_from_system) = The functions `query_as_update` and `system_task_as_update` turns a query function (note that composite query methods cannot be called when executing a message during this transition) resp the heartbeat or global timer into an update function; this is merely a notational trick to simplify the rule: ``` -query_as_update(f, arg, caller, env, available) = λ wasm_state → - match f(arg, caller, env, available)(wasm_state) with +query_as_update(f, arg, caller, caller_info_data, caller_info_signer, env, available) = λ wasm_state → + match f(arg, caller, caller_info_data, caller_info_signer, env, available)(wasm_state) with Trap trap → Trap trap Return res → Return { new_state = wasm_state; @@ -5540,6 +5666,12 @@ is_effective_canister_id(E.content, ECID) S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id Q.arg = candid(A) A.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) +if E.sender_pubkey = canister_signature_pk Signing_canister_id Seed: + if not (Q.sender_info = null): + verify_signature E.sender_pubkey Q.sender_info.sig ("\x0Eic-sender-info" · Q.sender_info.info) + Q.sender_info.signer = Signing_canister_id +else: + Q.sender_info = null Q.sender ∈ S.controllers[A.canister_id] ∪ S.subnet_admins[S.canister_subnet[A.canister_id]] ``` @@ -7291,10 +7423,33 @@ RM.origin = FromCanister { deadline = D } Ctxt_id ∈ dom(S.call_contexts) -not S.call_contexts[Ctxt_id].deleted -S.call_contexts[Ctxt_id].canister ∈ dom(S.balances) +Ctxt = S.call_contexts[Ctxt_id] +not Ctxt.deleted +Ctxt.canister ∈ dom(S.balances) D ≠ Expired _ +Caller = if Ctxt.origin = FromUser { request = R }: + R.sender +else if Ctxt.origin = FromCanister { calling_context = Calling_ctxt, …}: + S.call_contexts[Calling_ctxt].canister +else: + ic_principal + +if Ctxt.origin = FromUser { request = R }: + if R.sender = mk_self_authenticating_id (canister_signature_pk Signing_canister_id Seed): + if R.sender_info = null: + Caller_info_data = "" + Caller_info_signer = "" + else: + Caller_info_data = R.sender_info.info + Caller_info_signer = Signing_canister_id + else: + Caller_info_data = "" + Caller_info_signer = "" +else: + Caller_info_data = "" + Caller_info_signer = "" + ``` State after @@ -7308,6 +7463,9 @@ S with Older_messages · FuncMessage { call_context = Ctxt_id + caller = Caller + caller_info_data = Caller_info_data + caller_info_signer = Caller_info_signer receiver = S.call_contexts[Ctxt_id].canister entry_point = Callback Callback RM.response RM.refunded_cycles queue = Unordered @@ -7801,6 +7959,12 @@ is_effective_canister_id(E.content, ECID) S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id Q.arg = candid(A) A.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) +if E.sender_pubkey = canister_signature_pk Signing_canister_id Seed: + if not (Q.sender_info = null): + verify_signature E.sender_pubkey Q.sender_info.sig ("\x0Eic-sender-info" · Q.sender_info.info) + Q.sender_info.signer = Signing_canister_id +else: + Q.sender_info = null (S[A.canister_id].canister_log_visibility = Public) or (S[A.canister_id].canister_log_visibility = Controllers and Q.sender in S[A.canister_id].controllers) @@ -7825,9 +7989,58 @@ verify_response(Q, R, Cert) ∧ lookup(["time"], Cert) = Found S.system_time // ``` +#### IC Management Canister: List canisters (query call) {#ic-mgmt-canister-list-canisters} + +This section specifies the `list_canisters` management canister query call. +It is a call to `/api/v3/canister//query` +with CBOR content `Q` such that `Q.canister_id = ic_principal`. + +The management canister offers the method `list_canisters` +that can be called as a query call by subnet admins and +returns the list of all canisters on the subnet as consecutive canister ID ranges. + +Submitted request to `/api/v3/canister//query` + +```html + +E : Envelope + +``` + +Conditions + +```html + +E.content = CanisterQuery Q +Q.canister_id = ic_principal +Q.method_name = 'list_canisters' +|Q.nonce| <= 32 +is_effective_canister_id(E.content, ECID) +S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id +verify_envelope(E, Q.sender, S.system_time) +Q.sender ∈ S.subnet_admins[S.canister_subnet[ECID]] + +``` + +Query response `R`: + +```html + +{status: "replied"; reply: {arg: candid({canisters: CanisterIdRanges})}, signatures: Sigs} + +``` + +where `CanisterIdRanges` is the list of all canister IDs on the subnet encoded as consecutive canister ID ranges (excluding deleted canisters), and the query `Q`, the response `R`, and a certificate `Cert` that is obtained by requesting the path `/subnet` in a **separate** read state request to `/api/v3/canister//read_state` satisfy the following: + +```html + +verify_response(Q, R, Cert) ∧ lookup(["time"], Cert) = Found S.system_time // or "recent enough" + +``` + #### Query call {#query-call} -This section specifies query calls `Q` whose `Q.canister_id` is a non-empty canister `S.canisters[Q.canister_id]`. Query calls to the management canister, i.e., `Q.canister_id = ic_principal`, are specified in Section [Canister logs](#ic-mgmt-canister-fetch-canister-logs). +This section specifies query calls `Q` whose `Q.canister_id` is a non-empty canister `S.canisters[Q.canister_id]`. Query calls to the management canister, i.e., `Q.canister_id = ic_principal`, are specified in Sections [Canister status](#ic-management-canister-canister-status), [Canister logs](#ic-mgmt-canister-fetch-canister-logs), and [List canisters](#ic-mgmt-canister-list-canisters). Canister query calls to `/api/v3/canister//query` can be executed directly. They can only be executed against non-empty canisters which have a status of `Running` and are also not frozen. @@ -7837,7 +8050,7 @@ Composite query methods can call query methods and composite query methods up to We define an auxiliary method that handles calls from composite query methods by performing a call graph traversal. It can also be (trivially) invoked for query methods that do not make further calls. ``` -composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, Method_name, Arg) = +composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Caller_info_data, Caller_info_signer, Canister_id, Method_name, Arg) = let Mod = S.canisters[Canister_id].module let Cert <- { Cert | verify_cert(Cert) and lookup(["canister", Canister_id, "certified_data"], Cert) = Found S.certified_data[Canister_id] and @@ -7875,7 +8088,7 @@ composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, if liquid_balance(S, Canister_id) < 0 then Return (Reject (SYS_TRANSIENT, ), Cycles, S) - let R = F(Arg, Caller, Env)(W) + let R = F(Arg, Caller, Caller_info_data, Caller_info_signer, Env)(W) if R = Trap trap then Return (Reject (CANISTER_ERROR, ), Cycles - trap.cycles_used, S) else if R = Return {new_state = W'; new_calls = Calls; response = Response; cycles_used = Cycles_used} @@ -7897,14 +8110,14 @@ composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, if S.canister_subnet[Canister_id].subnet_id ≠ S.canister_subnet[Call.callee].subnet_id then Return (Reject (CANISTER_ERROR, ), Cycles, S) // calling to another subnet - let (Response', Cycles', S') = composite_query_helper(S, Cycles, Depth + 1, Root_canister_id, Canister_id, Call.callee, Call.method_name, Call.arg) + let (Response', Cycles', S') = composite_query_helper(S, Cycles, Depth + 1, Root_canister_id, Canister_id, "", "", Call.callee, Call.method_name, Call.arg) Cycles := Cycles' S := S' if Cycles < MAX_CYCLES_PER_RESPONSE then Return (Reject (CANISTER_ERROR, ), Cycles, S) // composite query out of cycles Env.Cert = NoCertificate // no certificate available in composite query callbacks - let F' = Mod.composite_callbacks(Call.callback, Response', Env) + let F' = Mod.composite_callbacks(Call.callback, Caller, Caller_info_data, Caller_info_signer, Response', Env) let R'' = F'(W') if R'' = Trap trap'' then Return (Reject (CANISTER_ERROR, ), Cycles - trap''.cycles_used, S) @@ -7941,20 +8154,37 @@ Conditions E.content = CanisterQuery Q Q.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) +if E.sender_pubkey = canister_signature_pk Signing_canister_id Seed: + if not (Q.sender_info = null): + verify_signature E.sender_pubkey Q.sender_info.sig ("\x0Eic-sender-info" · Q.sender_info.info) + Q.sender_info.signer = Signing_canister_id +else: + Q.sender_info = null |Q.nonce| <= 32 is_effective_canister_id(E.content, ECID) S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id +if Q.sender = mk_self_authenticating_id (canister_signature_pk Signing_canister_id Seed): + if Q.sender_info = null: + Caller_info_data = "" + Caller_info_signer = "" + else: + Caller_info_data = Q.sender_info.info + Caller_info_signer = Signing_canister_id +else: + Caller_info_data = "" + Caller_info_signer = "" + ``` Query response `R`: -- if `composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Q.canister_id, Q.method_name, Q.arg) = (Reject (RejectCode, RejectMsg), _, S')` then +- if `composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Caller_info_data, Caller_info_signer, Q.canister_id, Q.method_name, Q.arg) = (Reject (RejectCode, RejectMsg), _, S')` then ``` {status: "rejected"; reject_code: RejectCode; reject_message: RejectMsg; error_code: , signatures: Sigs} ``` -- Else if `composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Q.canister_id, Q.method_name, Q.arg) = (Reply Res, _, S')` then +- Else if `composite_query_helper(S, MAX_CYCLES_PER_QUERY, 0, Q.canister_id, Q.sender, Caller_info_data, Caller_info_signer, Q.canister_id, Q.method_name, Q.arg) = (Reply Res, _, S')` then ``` {status: "replied"; reply: {arg: Res}, signatures: Sigs} ``` @@ -8155,6 +8385,8 @@ We can model the execution of WebAssembly functions as stateful functions that h Params = { arg : NoArg | Blob; caller : Principal; + caller_info_data : Blob; + caller_info_signer : Blob; reject_code : 0 | SYS_FATAL | SYS_TRANSIENT | …; reject_message : Text; sysenv : Env; @@ -8207,6 +8439,8 @@ liquid_balance(es) = empty_params = { arg = NoArg; caller = ic_principal; + caller_info_data = ""; + caller_info_signer = ""; reject_code = 0; reject_message = ""; sysenv = (undefined); @@ -8385,12 +8619,14 @@ Finally, we can specify the abstract `CanisterModule` that models a concrete Web - The partial map `update_methods` of the `CanisterModule` is defined for all method names `method` for which the WebAssembly program exports a function `func` named `canister_update `, and has value ``` - update_methods[method] = λ (arg, caller, deadline, sysenv, available) → λ wasm_state → + update_methods[method] = λ (arg, caller, caller_info_data, caller_info_signer, deadline, sysenv, available) → λ wasm_state → let es = ref {empty_execution_state with wasm_state = wasm_state; params = empty_params with { arg = arg; caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; deadline = deadline; sysenv; } @@ -8413,10 +8649,16 @@ Finally, we can specify the abstract `CanisterModule` that models a concrete Web - The partial map `query_methods` of the `CanisterModule` is defined for all method names `method` for which the WebAssembly program exports a function `func` named `canister_query `, and has value ``` - query_methods[method] = λ (arg, caller, sysenv, available) → λ wasm_state → + query_methods[method] = λ (arg, caller, caller_info_data, caller_info_signer, sysenv, available) → λ wasm_state → let es = ref {empty_execution_state with wasm_state = wasm_state; - params = empty_params with { arg = arg; caller = caller; sysenv } + params = empty_params with { + arg = arg; + caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; + sysenv + } balance = sysenv.balance cycles_available = available context = Q @@ -8433,10 +8675,16 @@ Finally, we can specify the abstract `CanisterModule` that models a concrete Web - The partial map `composite_query_methods` of the `CanisterModule` is defined for all method names `method` for which the WebAssembly program exports a function `func` named `canister_composite_query `, and has value ``` - composite_query_methods[method] = λ (arg, caller, sysenv) → λ wasm_state → + composite_query_methods[method] = λ (arg, caller, caller_info_data, caller_info_signer, sysenv) → λ wasm_state → let es = ref {empty_execution_state with wasm_state = wasm_state; - params = empty_params with { arg = arg; caller = caller; sysenv } + params = empty_params with { + arg = arg; + caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; + sysenv + } balance = sysenv.balance context = CQ } @@ -8535,8 +8783,11 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} - The function `callbacks` of the `CanisterModule` is defined as follows ``` I ∈ {i32, i64} - callbacks = λ(callbacks, response, deadline, refunded_cycles, sysenv, available) → λ wasm_state → + callbacks = λ(callbacks, caller, caller_info_data, caller_info_signer, response, deadline, refunded_cycles, sysenv, available) → λ wasm_state → let params0 = empty_params with { + caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; sysenv; cycles_refunded = refund_cycles; deadline; @@ -8578,6 +8829,8 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} let es' = ref { empty_execution_state with wasm_state = wasm_state; + params = params; + balance = sysenv.balance - es.cycles_used; context = C; } try func(callbacks.on_cleanup.env) with Trap then Trap {cycles_used = es.cycles_used + es'.cycles_used;} @@ -8597,8 +8850,11 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} - The function `composite_callbacks` of the `CanisterModule` is defined as follows ``` I ∈ {i32, i64} - composite_callbacks = λ(callbacks, response, sysenv) → λ wasm_state → + composite_callbacks = λ(callbacks, caller, caller_info_data, caller_info_signer, response, sysenv) → λ wasm_state → let params0 = empty_params with { + caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; sysenv } let (fun, env, params, context) = match response with @@ -8634,6 +8890,8 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} let es' = ref { empty_execution_state with wasm_state = wasm_state; + params = params; + balance = sysenv.balance - es.cycles_used; context = CC; } try func(callbacks.on_cleanup.env) with Trap then Trap {cycles_used = es.cycles_used + es'.cycles_used;} @@ -8651,18 +8909,20 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} If the WebAssembly module does not export a function called under the name `canister_inspect_message`, then access is always granted: ``` - inspect_message = λ (method_name, wasm_state, arg, caller, sysenv) → + inspect_message = λ (method_name, wasm_state, arg, caller, caller_info_data, caller_info_signer, sysenv) → Return {status = Accept;} ``` Otherwise, if the WebAssembly module exports a function `func` under the name `canister_inspect_message`, it is ``` - inspect_message = λ (method_name, wasm_state, arg, caller, sysenv) → + inspect_message = λ (method_name, wasm_state, arg, caller, caller_info_data, caller_info_signer, sysenv) → let es = ref {empty_execution_state with wasm_state = wasm_state; params = empty_params with { arg = arg; caller = caller; + caller_info_data = caller_info_data; + caller_info_signer = caller_info_signer; method_name = method_name; sysenv } @@ -8734,6 +8994,26 @@ ic0.msg_caller_copy(dst : I, offset : I, size : I) = if es.context = s then Trap {cycles_used = es.cycles_used;} copy_to_canister(dst, offset, size, es.params.caller) +I ∈ {i32, i64} +ic0.msg_caller_info_data_size() : I = + if es.context ∉ {U, RQ, NRQ, CQ, Ry, Rt, CRy, CRt, C, CC, F} then Trap {cycles_used = es.cycles_used;} + return |es.params.caller_info_data| + +I ∈ {i32, i64} +ic0.msg_caller_info_data_copy(dst : I, offset : I, size : I) = + if es.context ∉ {U, RQ, NRQ, CQ, Ry, Rt, CRy, CRt, C, CC, F} then Trap {cycles_used = es.cycles_used;} + copy_to_canister(dst, offset, size, es.params.caller_info_data) + +I ∈ {i32, i64} +ic0.msg_caller_info_signer_size() : I = + if es.context ∉ {U, RQ, NRQ, CQ, Ry, Rt, CRy, CRt, C, CC, F} then Trap {cycles_used = es.cycles_used;} + return |es.params.caller_info_signer| + +I ∈ {i32, i64} +ic0.msg_caller_info_signer_copy(dst : I, offset : I, size : I) = + if es.context ∉ {U, RQ, NRQ, CQ, Ry, Rt, CRy, CRt, C, CC, F} then Trap {cycles_used = es.cycles_used;} + copy_to_canister(dst, offset, size, es.params.caller_info_signer) + ic0.msg_reject_code() : i32 = if es.context ∉ {Ry, Rt, CRy, CRt, C} then Trap {cycles_used = es.cycles_used;} es.params.reject_code diff --git a/docs/reference/management-canister.md b/docs/reference/management-canister.md index b6150f3d..091cfa06 100644 --- a/docs/reference/management-canister.md +++ b/docs/reference/management-canister.md @@ -560,6 +560,17 @@ Returns metadata about a subnet. - `replica_version` (`text`): the replica version running on the subnet - `registry_version` (`nat64`): the registry version of the subnet +### `list_canisters` + +Returns all canisters hosted on the caller's subnet as a list of consecutive canister ID ranges. Deleted canisters are not included. Only callable by subnet admins as a query call; not callable by canisters, via replicated calls, or from composite query calls. + +- **Caller:** Subnet admins only (query call; not callable by canisters) +- **Parameters:** none +- **Returns:** + - `canisters` (`vec record { start : principal; end : principal }`): contiguous ranges of canister IDs where `start` and `end` are both inclusive + +> The response comes from a single replica and is not suitable for security-sensitive applications. Consider replica-signed queries if verification of the subnet origin is required. + ## Provisional methods (local testing only) These methods are only available on local development instances. They do not exist on mainnet. diff --git a/public/reference/ic.did b/public/reference/ic.did index 7ff80d32..cad1d2b4 100644 --- a/public/reference/ic.did +++ b/public/reference/ic.did @@ -511,6 +511,15 @@ type fetch_canister_logs_result = record { canister_log_records : vec canister_log_record; }; +type canister_id_range = record { + start : canister_id; + end : canister_id; +}; + +type list_canisters_result = record { + canisters : vec canister_id_range; +}; + type read_canister_snapshot_metadata_args = record { canister_id : canister_id; snapshot_id : snapshot_id; @@ -683,4 +692,7 @@ service ic : { // canister logging fetch_canister_logs : (fetch_canister_logs_args) -> (fetch_canister_logs_result) query; + + // subnet admin methods + list_canisters : () -> (list_canisters_result) query; };