diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bc1d453..7d5ca2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added node signature certification to query calls, for protection against rogue boundary nodes. This can be disabled with `with_verify_query_signatures`. +* Added `with_nonce_generation` to `QueryBuilder` for precise cache control. * Added `read_subnet_state_raw` to `Agent` and `read_subnet_state` to `Transport` for looking up raw state by subnet ID instead of canister ID. * Added `read_state_subnet_metrics` to `Agent` to access subnet metrics, such as total spent cycles. * Types passed to the `to_request_id` function can now contain nested structs, signed integers, and externally tagged enums. diff --git a/ic-agent/src/agent/agent_test.rs b/ic-agent/src/agent/agent_test.rs index 10401813..28976d67 100644 --- a/ic-agent/src/agent/agent_test.rs +++ b/ic-agent/src/agent/agent_test.rs @@ -68,6 +68,7 @@ async fn query() -> Result<(), AgentError> { "main".to_string(), vec![], None, + false, ) .await; @@ -92,6 +93,7 @@ async fn query_error() -> Result<(), AgentError> { "greet".to_string(), vec![], None, + false, ) .await; @@ -132,6 +134,7 @@ async fn query_rejected() -> Result<(), AgentError> { "greet".to_string(), vec![], None, + false, ) .await; diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index ab40b1f8..e5ea0aad 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -433,8 +433,15 @@ impl Agent { method_name: String, arg: Vec, ingress_expiry_datetime: Option, + use_nonce: bool, ) -> Result, AgentError> { - let content = self.query_content(canister_id, method_name, arg, ingress_expiry_datetime)?; + let content = self.query_content( + canister_id, + method_name, + arg, + ingress_expiry_datetime, + use_nonce, + )?; let serialized_bytes = sign_envelope(&content, self.identity.clone())?; self.query_inner( effective_canister_id, @@ -534,6 +541,7 @@ impl Agent { method_name: String, arg: Vec, ingress_expiry_datetime: Option, + use_nonce: bool, ) -> Result { Ok(EnvelopeContent::Query { sender: self.identity.sender().map_err(AgentError::SigningError)?, @@ -541,6 +549,7 @@ impl Agent { method_name, arg, ingress_expiry: ingress_expiry_datetime.unwrap_or_else(|| self.get_expiry_date()), + nonce: use_nonce.then(|| self.nonce_factory.generate()).flatten(), }) } @@ -1050,6 +1059,7 @@ pub fn signed_query_inspect( canister_id: canister_id_cbor, method_name: method_name_cbor, arg: arg_cbor, + nonce: _nonce, } => { if ingress_expiry != *ingress_expiry_cbor { return Err(AgentError::CallDataMismatch { @@ -1310,6 +1320,7 @@ pub(crate) struct Subnet { /// /// This makes it easier to do query calls without actually passing all arguments. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct QueryBuilder<'agent> { agent: &'agent Agent, /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination. @@ -1322,6 +1333,8 @@ pub struct QueryBuilder<'agent> { pub arg: Vec, /// The Unix timestamp that the request will expire at. pub ingress_expiry_datetime: Option, + /// Whether to include a nonce with the message. + pub use_nonce: bool, } impl<'agent> QueryBuilder<'agent> { @@ -1334,6 +1347,7 @@ impl<'agent> QueryBuilder<'agent> { method_name, arg: vec![], ingress_expiry_datetime: None, + use_nonce: false, } } @@ -1368,6 +1382,13 @@ impl<'agent> QueryBuilder<'agent> { self } + /// Uses a nonce generated with the agent's configured nonce factory. By default queries do not use nonces, + /// and thus may get a (briefly) cached response. + pub fn with_nonce_generation(mut self) -> Self { + self.use_nonce = true; + self + } + /// Make a query call. This will return a byte vector. pub async fn call(self) -> Result, AgentError> { self.agent @@ -1377,6 +1398,7 @@ impl<'agent> QueryBuilder<'agent> { self.method_name, self.arg, self.ingress_expiry_datetime, + self.use_nonce, ) .await } @@ -1389,6 +1411,7 @@ impl<'agent> QueryBuilder<'agent> { self.method_name, self.arg, self.ingress_expiry_datetime, + self.use_nonce, )?; let signed_query = sign_envelope(&content, self.agent.identity.clone())?; let EnvelopeContent::Query { @@ -1397,6 +1420,7 @@ impl<'agent> QueryBuilder<'agent> { canister_id, method_name, arg, + nonce, } = content else { unreachable!() @@ -1409,6 +1433,7 @@ impl<'agent> QueryBuilder<'agent> { arg, effective_canister_id: self.effective_canister_id, signed_query, + nonce, }) } } diff --git a/ic-transport-types/src/lib.rs b/ic-transport-types/src/lib.rs index 4752e776..9504abdf 100644 --- a/ic-transport-types/src/lib.rs +++ b/ic-transport-types/src/lib.rs @@ -77,6 +77,9 @@ pub enum EnvelopeContent { /// The argument to pass to the canister method. #[serde(with = "serde_bytes")] arg: Vec, + /// A random series of bytes to uniquely identify this message. + #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")] + nonce: Option>, }, } diff --git a/ic-transport-types/src/signed.rs b/ic-transport-types/src/signed.rs index 412f6d9a..468cb910 100644 --- a/ic-transport-types/src/signed.rs +++ b/ic-transport-types/src/signed.rs @@ -24,6 +24,11 @@ pub struct SignedQuery { /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. #[serde(with = "serde_bytes")] pub signed_query: Vec, + /// A nonce to uniquely identify this query call. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "serde_bytes")] + pub nonce: Option>, } /// A signed update request message. Produced by @@ -87,6 +92,7 @@ mod tests { arg: vec![0, 1], effective_canister_id: Principal::management_canister(), signed_query: vec![0, 1, 2, 3], + nonce: None, }; let serialized = serde_json::to_string(&query).unwrap(); let deserialized = serde_json::from_str::(&serialized); diff --git a/ref-tests/tests/ic-ref.rs b/ref-tests/tests/ic-ref.rs index a8c98c77..0fee8cfc 100644 --- a/ref-tests/tests/ic-ref.rs +++ b/ref-tests/tests/ic-ref.rs @@ -858,9 +858,18 @@ mod simple_calls { fn query() { with_universal_canister(|agent, canister_id| async move { let arg = payload().reply_data(b"hello").build(); + let result = agent + .query(&canister_id, "query") + .with_arg(arg.clone()) + .call() + .await?; + + assert_eq!(result, b"hello"); + let result = agent .query(&canister_id, "query") .with_arg(arg) + .with_nonce_generation() .call() .await?;