Skip to content

Commit 7a109b9

Browse files
feat: Add encoding functions for external signing (#600)
1 parent 8967a7f commit 7a109b9

File tree

6 files changed

+60
-22
lines changed

6 files changed

+60
-22
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## Unreleased
1010

11+
* Added `Envelope::encode_bytes` and `Query/UpdateBuilder::into_envelope` for external signing workflows.
1112
* Added `AgentBuilder::with_arc_http_middleware` for `Transport`-like functionality at the level of HTTP requests.
1213
* Add support for dynamic routing based on boundary node discovery. This is an internal feature for now, with a feature flag `_internal_dynamic-routing`.
1314

Diff for: Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ic-agent/src/agent/mod.rs

+35-21
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@ pub struct ApiBoundaryNode {
15091509
pub ipv4_address: Option<String>,
15101510
}
15111511

1512-
/// A Query Request Builder.
1512+
/// A query request builder.
15131513
///
15141514
/// This makes it easier to do query calls without actually passing all arguments.
15151515
#[derive(Debug, Clone)]
@@ -1633,14 +1633,10 @@ impl<'agent> QueryBuilder<'agent> {
16331633
/// Sign a query call. This will return a [`signed::SignedQuery`]
16341634
/// which contains all fields of the query and the signed query in CBOR encoding
16351635
pub fn sign(self) -> Result<SignedQuery, AgentError> {
1636-
let content = self.agent.query_content(
1637-
self.canister_id,
1638-
self.method_name,
1639-
self.arg,
1640-
self.ingress_expiry_datetime,
1641-
self.use_nonce,
1642-
)?;
1643-
let signed_query = sign_envelope(&content, self.agent.identity.clone())?;
1636+
let effective_canister_id = self.effective_canister_id;
1637+
let identity = self.agent.identity.clone();
1638+
let content = self.into_envelope()?;
1639+
let signed_query = sign_envelope(&content, identity)?;
16441640
let EnvelopeContent::Query {
16451641
ingress_expiry,
16461642
sender,
@@ -1658,11 +1654,22 @@ impl<'agent> QueryBuilder<'agent> {
16581654
canister_id,
16591655
method_name,
16601656
arg,
1661-
effective_canister_id: self.effective_canister_id,
1657+
effective_canister_id,
16621658
signed_query,
16631659
nonce,
16641660
})
16651661
}
1662+
1663+
/// Converts the query builder into [`EnvelopeContent`] for external signing or storage.
1664+
pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
1665+
self.agent.query_content(
1666+
self.canister_id,
1667+
self.method_name,
1668+
self.arg,
1669+
self.ingress_expiry_datetime,
1670+
self.use_nonce,
1671+
)
1672+
}
16661673
}
16671674

16681675
impl<'agent> IntoFuture for QueryBuilder<'agent> {
@@ -1709,7 +1716,7 @@ impl<'a> UpdateCall<'a> {
17091716
}
17101717
}
17111718
}
1712-
/// An Update Request Builder.
1719+
/// An update request Builder.
17131720
///
17141721
/// This makes it easier to do update calls without actually passing all arguments or specifying
17151722
/// if you want to wait or not.
@@ -1799,15 +1806,10 @@ impl<'agent> UpdateBuilder<'agent> {
17991806
/// Sign a update call. This will return a [`signed::SignedUpdate`]
18001807
/// which contains all fields of the update and the signed update in CBOR encoding
18011808
pub fn sign(self) -> Result<SignedUpdate, AgentError> {
1802-
let nonce = self.agent.nonce_factory.generate();
1803-
let content = self.agent.update_content(
1804-
self.canister_id,
1805-
self.method_name,
1806-
self.arg,
1807-
self.ingress_expiry_datetime,
1808-
nonce,
1809-
)?;
1810-
let signed_update = sign_envelope(&content, self.agent.identity.clone())?;
1809+
let identity = self.agent.identity.clone();
1810+
let effective_canister_id = self.effective_canister_id;
1811+
let content = self.into_envelope()?;
1812+
let signed_update = sign_envelope(&content, identity)?;
18111813
let request_id = to_request_id(&content)?;
18121814
let EnvelopeContent::Call {
18131815
nonce,
@@ -1827,11 +1829,23 @@ impl<'agent> UpdateBuilder<'agent> {
18271829
canister_id,
18281830
method_name,
18291831
arg,
1830-
effective_canister_id: self.effective_canister_id,
1832+
effective_canister_id,
18311833
signed_update,
18321834
request_id,
18331835
})
18341836
}
1837+
1838+
/// Converts the update builder into an [`EnvelopeContent`] for external signing or storage.
1839+
pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
1840+
let nonce = self.agent.nonce_factory.generate();
1841+
self.agent.update_content(
1842+
self.canister_id,
1843+
self.method_name,
1844+
self.arg,
1845+
self.ingress_expiry_datetime,
1846+
nonce,
1847+
)
1848+
}
18351849
}
18361850

18371851
impl<'agent> IntoFuture for UpdateBuilder<'agent> {

Diff for: ic-transport-types/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ leb128.workspace = true
1818
thiserror.workspace = true
1919
serde.workspace = true
2020
serde_bytes.workspace = true
21+
serde_cbor.workspace = true
2122
serde_repr.workspace = true
2223
sha2.workspace = true
2324

Diff for: ic-transport-types/src/lib.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use thiserror::Error;
1616
mod request_id;
1717
pub mod signed;
1818

19-
/// The authentication envelope, containing the contents and their signature.
19+
/// The authentication envelope, containing the contents and their signature. This struct can be passed to `Agent`'s
20+
/// `*_signed` methods via [`to_bytes`](Envelope::to_bytes).
2021
#[derive(Debug, Clone, Deserialize, Serialize)]
2122
#[serde(rename_all = "snake_case")]
2223
pub struct Envelope<'a> {
@@ -34,6 +35,17 @@ pub struct Envelope<'a> {
3435
pub sender_delegation: Option<Vec<SignedDelegation>>,
3536
}
3637

38+
impl Envelope<'_> {
39+
/// Convert the authentication envelope to the format expected by the IC HTTP interface. The result can be passed to `Agent`'s `*_signed` methods.
40+
pub fn encode_bytes(&self) -> Vec<u8> {
41+
let mut serializer = serde_cbor::Serializer::new(Vec::new());
42+
serializer.self_describe().unwrap();
43+
self.serialize(&mut serializer)
44+
.expect("infallible Envelope::serialize");
45+
serializer.into_inner()
46+
}
47+
}
48+
3749
/// The content of an IC ingress message, not including any signature information.
3850
#[derive(Debug, Clone, Serialize, Deserialize)]
3951
#[serde(tag = "request_type", rename_all = "snake_case")]

Diff for: ic-transport-types/src/signed.rs

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
66

77
/// A signed query request message. Produced by
88
/// [`QueryBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.QueryBuilder.html#method.sign).
9+
///
10+
/// 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).
911
#[derive(Debug, Clone, Deserialize, Serialize)]
1012
pub struct SignedQuery {
1113
/// The Unix timestamp that the request will expire at.
@@ -22,6 +24,7 @@ pub struct SignedQuery {
2224
/// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
2325
pub effective_canister_id: Principal,
2426
/// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request.
27+
/// This field can be passed to [`Agent::query_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.query_signed).
2528
#[serde(with = "serde_bytes")]
2629
pub signed_query: Vec<u8>,
2730
/// A nonce to uniquely identify this query call.
@@ -33,6 +36,8 @@ pub struct SignedQuery {
3336

3437
/// A signed update request message. Produced by
3538
/// [`UpdateBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.UpdateBuilder.html#method.sign).
39+
///
40+
/// 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).
3641
#[derive(Debug, Clone, Deserialize, Serialize)]
3742
pub struct SignedUpdate {
3843
/// A nonce to uniquely identify this update call.
@@ -55,13 +60,16 @@ pub struct SignedUpdate {
5560
pub effective_canister_id: Principal,
5661
#[serde(with = "serde_bytes")]
5762
/// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request.
63+
/// This field can be passed to [`Agent::update_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.update_signed).
5864
pub signed_update: Vec<u8>,
5965
/// The request ID.
6066
pub request_id: RequestId,
6167
}
6268

6369
/// A signed request-status request message. Produced by
6470
/// [`Agent::sign_request_status`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.Agent.html#method.sign_request_status).
71+
///
72+
/// 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).
6573
#[derive(Debug, Clone, Deserialize, Serialize)]
6674
pub struct SignedRequestStatus {
6775
/// The Unix timestamp that the request will expire at.
@@ -73,6 +81,7 @@ pub struct SignedRequestStatus {
7381
/// The request ID.
7482
pub request_id: RequestId,
7583
/// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request.
84+
/// 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).
7685
#[serde(with = "serde_bytes")]
7786
pub signed_request_status: Vec<u8>,
7887
}

0 commit comments

Comments
 (0)