From 7a109b90a52ee59191d51a3a492d6bd368ffa561 Mon Sep 17 00:00:00 2001 From: Adam Spofford <93943719+adamspofford-dfinity@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:29:39 -0700 Subject: [PATCH] feat: Add encoding functions for external signing (#600) --- CHANGELOG.md | 1 + Cargo.lock | 1 + ic-agent/src/agent/mod.rs | 56 ++++++++++++++++++++------------ ic-transport-types/Cargo.toml | 1 + ic-transport-types/src/lib.rs | 14 +++++++- ic-transport-types/src/signed.rs | 9 +++++ 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8832305..6cdd3341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added `Envelope::encode_bytes` and `Query/UpdateBuilder::into_envelope` for external signing workflows. * Added `AgentBuilder::with_arc_http_middleware` for `Transport`-like functionality at the level of HTTP requests. * Add support for dynamic routing based on boundary node discovery. This is an internal feature for now, with a feature flag `_internal_dynamic-routing`. diff --git a/Cargo.lock b/Cargo.lock index c23c2603..e68ebca9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "leb128", "serde", "serde_bytes", + "serde_cbor", "serde_json", "serde_repr", "sha2 0.10.8", diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index 41f82da6..0978652f 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -1509,7 +1509,7 @@ pub struct ApiBoundaryNode { pub ipv4_address: Option, } -/// A Query Request Builder. +/// A query request builder. /// /// This makes it easier to do query calls without actually passing all arguments. #[derive(Debug, Clone)] @@ -1633,14 +1633,10 @@ impl<'agent> QueryBuilder<'agent> { /// Sign a query call. This will return a [`signed::SignedQuery`] /// which contains all fields of the query and the signed query in CBOR encoding pub fn sign(self) -> Result { - let content = self.agent.query_content( - self.canister_id, - self.method_name, - self.arg, - self.ingress_expiry_datetime, - self.use_nonce, - )?; - let signed_query = sign_envelope(&content, self.agent.identity.clone())?; + let effective_canister_id = self.effective_canister_id; + let identity = self.agent.identity.clone(); + let content = self.into_envelope()?; + let signed_query = sign_envelope(&content, identity)?; let EnvelopeContent::Query { ingress_expiry, sender, @@ -1658,11 +1654,22 @@ impl<'agent> QueryBuilder<'agent> { canister_id, method_name, arg, - effective_canister_id: self.effective_canister_id, + effective_canister_id, signed_query, nonce, }) } + + /// Converts the query builder into [`EnvelopeContent`] for external signing or storage. + pub fn into_envelope(self) -> Result { + self.agent.query_content( + self.canister_id, + self.method_name, + self.arg, + self.ingress_expiry_datetime, + self.use_nonce, + ) + } } impl<'agent> IntoFuture for QueryBuilder<'agent> { @@ -1709,7 +1716,7 @@ impl<'a> UpdateCall<'a> { } } } -/// An Update Request Builder. +/// An update request Builder. /// /// This makes it easier to do update calls without actually passing all arguments or specifying /// if you want to wait or not. @@ -1799,15 +1806,10 @@ impl<'agent> UpdateBuilder<'agent> { /// Sign a update call. This will return a [`signed::SignedUpdate`] /// which contains all fields of the update and the signed update in CBOR encoding pub fn sign(self) -> Result { - let nonce = self.agent.nonce_factory.generate(); - let content = self.agent.update_content( - self.canister_id, - self.method_name, - self.arg, - self.ingress_expiry_datetime, - nonce, - )?; - let signed_update = sign_envelope(&content, self.agent.identity.clone())?; + let identity = self.agent.identity.clone(); + let effective_canister_id = self.effective_canister_id; + let content = self.into_envelope()?; + let signed_update = sign_envelope(&content, identity)?; let request_id = to_request_id(&content)?; let EnvelopeContent::Call { nonce, @@ -1827,11 +1829,23 @@ impl<'agent> UpdateBuilder<'agent> { canister_id, method_name, arg, - effective_canister_id: self.effective_canister_id, + effective_canister_id, signed_update, request_id, }) } + + /// Converts the update builder into an [`EnvelopeContent`] for external signing or storage. + pub fn into_envelope(self) -> Result { + let nonce = self.agent.nonce_factory.generate(); + self.agent.update_content( + self.canister_id, + self.method_name, + self.arg, + self.ingress_expiry_datetime, + nonce, + ) + } } impl<'agent> IntoFuture for UpdateBuilder<'agent> { diff --git a/ic-transport-types/Cargo.toml b/ic-transport-types/Cargo.toml index 3fc529cb..8c706913 100644 --- a/ic-transport-types/Cargo.toml +++ b/ic-transport-types/Cargo.toml @@ -18,6 +18,7 @@ leb128.workspace = true thiserror.workspace = true serde.workspace = true serde_bytes.workspace = true +serde_cbor.workspace = true serde_repr.workspace = true sha2.workspace = true diff --git a/ic-transport-types/src/lib.rs b/ic-transport-types/src/lib.rs index d5d3875f..881897d9 100644 --- a/ic-transport-types/src/lib.rs +++ b/ic-transport-types/src/lib.rs @@ -16,7 +16,8 @@ use thiserror::Error; mod request_id; pub mod signed; -/// The authentication envelope, containing the contents and their signature. +/// The authentication envelope, containing the contents and their signature. This struct can be passed to `Agent`'s +/// `*_signed` methods via [`to_bytes`](Envelope::to_bytes). #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct Envelope<'a> { @@ -34,6 +35,17 @@ pub struct Envelope<'a> { pub sender_delegation: Option>, } +impl Envelope<'_> { + /// Convert the authentication envelope to the format expected by the IC HTTP interface. The result can be passed to `Agent`'s `*_signed` methods. + pub fn encode_bytes(&self) -> Vec { + let mut serializer = serde_cbor::Serializer::new(Vec::new()); + serializer.self_describe().unwrap(); + self.serialize(&mut serializer) + .expect("infallible Envelope::serialize"); + serializer.into_inner() + } +} + /// The content of an IC ingress message, not including any signature information. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "request_type", rename_all = "snake_case")] diff --git a/ic-transport-types/src/signed.rs b/ic-transport-types/src/signed.rs index 468cb910..aa2261da 100644 --- a/ic-transport-types/src/signed.rs +++ b/ic-transport-types/src/signed.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; /// A signed query request message. Produced by /// [`QueryBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.QueryBuilder.html#method.sign). +/// +/// To submit this request, pass the `signed_query` field to [`Agent::query_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.query_signed). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SignedQuery { /// The Unix timestamp that the request will expire at. @@ -22,6 +24,7 @@ pub struct SignedQuery { /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination. pub effective_canister_id: Principal, /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. + /// This field can be passed to [`Agent::query_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.query_signed). #[serde(with = "serde_bytes")] pub signed_query: Vec, /// A nonce to uniquely identify this query call. @@ -33,6 +36,8 @@ pub struct SignedQuery { /// A signed update request message. Produced by /// [`UpdateBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.UpdateBuilder.html#method.sign). +/// +/// To submit this request, pass the `signed_update` field to [`Agent::update_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.update_signed). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SignedUpdate { /// A nonce to uniquely identify this update call. @@ -55,6 +60,7 @@ pub struct SignedUpdate { pub effective_canister_id: Principal, #[serde(with = "serde_bytes")] /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. + /// This field can be passed to [`Agent::update_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.update_signed). pub signed_update: Vec, /// The request ID. pub request_id: RequestId, @@ -62,6 +68,8 @@ pub struct SignedUpdate { /// A signed request-status request message. Produced by /// [`Agent::sign_request_status`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.Agent.html#method.sign_request_status). +/// +/// To submit this request, pass the `signed_request_status` field to [`Agent::request_status_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.request_status_signed). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SignedRequestStatus { /// The Unix timestamp that the request will expire at. @@ -73,6 +81,7 @@ pub struct SignedRequestStatus { /// The request ID. pub request_id: RequestId, /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. + /// This field can be passed to [`Agent::request_status_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.request_status_signed). #[serde(with = "serde_bytes")] pub signed_request_status: Vec, }