ic_backup/model/ic_request/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4mod method;
5pub use method::{IcManagementMethodRecord, IcRequestEffect};
6
7use crate::model::{
8 artifacts::ArtifactChecksumRecord, attempt_journal::OperationBindingRecord, bounded_sequence,
9};
10use ic_management_canister_types::{
11 CanisterIdRecord, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs,
12};
13use serde::{Deserialize, Deserializer, Serialize};
14use thiserror::Error;
15
16pub const MAX_IC_SNAPSHOT_ID_BYTES: usize = 256;
18pub const MAX_IC_ARGUMENT_BYTES: usize = 4096;
20pub const MAX_IC_REQUEST_RECORD_BYTES: u64 = 8192;
22
23#[derive(Clone, Debug)]
25pub struct IcManagementRequest {
26 pub method: IcManagementMethodRecord,
28 pub target: String,
30 pub snapshot_id: Option<Vec<u8>>,
32}
33
34#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40#[serde(try_from = "RequestFields")]
41pub struct IcManagementRequestRecord {
42 version: u16,
43 method: IcManagementMethodRecord,
44 target: String,
45 snapshot_id: Option<Vec<u8>>,
46 #[serde(skip)]
47 target_bytes: Vec<u8>,
48 #[serde(skip)]
49 arguments: Vec<u8>,
50}
51#[derive(Deserialize)]
52#[serde(deny_unknown_fields)]
53struct RequestFields {
54 version: u16,
55 method: IcManagementMethodRecord,
56 target: String,
57 #[serde(deserialize_with = "required_snapshot")]
58 snapshot_id: Option<Vec<u8>>,
59}
60impl TryFrom<RequestFields> for IcManagementRequestRecord {
61 type Error = IcRequestError;
62 fn try_from(fields: RequestFields) -> Result<Self, Self::Error> {
63 if fields.version != 1 {
64 return Err(IcRequestError::UnsupportedVersion(fields.version));
65 }
66 Self::new(IcManagementRequest {
67 method: fields.method,
68 target: fields.target,
69 snapshot_id: fields.snapshot_id,
70 })
71 }
72}
73impl IcManagementRequestRecord {
74 pub fn new(request: IcManagementRequest) -> Result<Self, IcRequestError> {
83 let (target, principal) = crate::model::principal::canonical_parts(&request.target)
84 .ok_or(IcRequestError::InvalidTarget)?;
85 match (request.method, request.snapshot_id.as_deref()) {
86 (IcManagementMethodRecord::LoadCanisterSnapshot, None) => {
87 return Err(IcRequestError::SnapshotRequired);
88 }
89 (IcManagementMethodRecord::LoadCanisterSnapshot, Some(bytes)) => {
90 if bytes.is_empty() || bytes.len() > MAX_IC_SNAPSHOT_ID_BYTES {
91 return Err(IcRequestError::InvalidSnapshotId);
92 }
93 }
94 (_, Some(_)) => return Err(IcRequestError::UnexpectedSnapshot),
95 (_, None) => {}
96 }
97 let arguments = match request.method {
98 IcManagementMethodRecord::TakeCanisterSnapshot => {
99 candid::encode_one(TakeCanisterSnapshotArgs {
100 canister_id: principal,
101 replace_snapshot: None,
102 uninstall_code: Some(false),
103 sender_canister_version: None,
104 })
105 }
106 IcManagementMethodRecord::LoadCanisterSnapshot => {
107 candid::encode_one(LoadCanisterSnapshotArgs {
108 canister_id: principal,
109 snapshot_id: request
110 .snapshot_id
111 .clone()
112 .ok_or(IcRequestError::SnapshotRequired)?,
113 sender_canister_version: None,
114 })
115 }
116 IcManagementMethodRecord::CanisterStatus
117 | IcManagementMethodRecord::ListCanisterSnapshots
118 | IcManagementMethodRecord::StartCanister
119 | IcManagementMethodRecord::StopCanister => candid::encode_one(CanisterIdRecord {
120 canister_id: principal,
121 }),
122 }
123 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
124 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
125 return Err(IcRequestError::ArgumentsTooLarge);
126 }
127 Ok(Self {
128 version: 1,
129 method: request.method,
130 target,
131 snapshot_id: request.snapshot_id,
132 target_bytes: principal.as_slice().to_vec(),
133 arguments,
134 })
135 }
136 #[must_use]
138 pub const fn method(&self) -> IcManagementMethodRecord {
139 self.method
140 }
141 #[must_use]
143 pub fn target(&self) -> &str {
144 &self.target
145 }
146 #[must_use]
148 pub fn snapshot_id(&self) -> Option<&[u8]> {
149 self.snapshot_id.as_deref()
150 }
151 #[must_use]
153 pub fn arguments(&self) -> &[u8] {
154 &self.arguments
155 }
156 #[must_use]
158 pub const fn receiver(&self) -> &'static str {
159 "aaaaa-aa"
160 }
161 #[must_use]
166 pub fn digest(&self) -> ArtifactChecksumRecord {
167 management_request_digest(&self.target_bytes, self.method.name(), &self.arguments)
168 }
169 pub fn validate_mutation_binding(
175 &self,
176 binding: &OperationBindingRecord,
177 ) -> Result<(), IcRequestError> {
178 self.require_effect(IcRequestEffect::Mutation)?;
179 self.validate_identity(binding, binding.request())
180 }
181 pub fn validate_observation_binding(
188 &self,
189 binding: &OperationBindingRecord,
190 request: &ArtifactChecksumRecord,
191 ) -> Result<(), IcRequestError> {
192 self.require_effect(IcRequestEffect::Observation)?;
193 self.validate_identity(binding, request.hash())
194 }
195 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
196 if self.method.effect() != expected {
197 return Err(IcRequestError::EffectMismatch { expected });
198 }
199 Ok(())
200 }
201 fn validate_identity(
202 &self,
203 binding: &OperationBindingRecord,
204 expected: &str,
205 ) -> Result<(), IcRequestError> {
206 if self.target != binding.target() {
207 return Err(IcRequestError::TargetMismatch);
208 }
209 if self.digest().hash() != expected {
210 return Err(IcRequestError::DigestMismatch);
211 }
212 Ok(())
213 }
214}
215
216pub(super) fn management_request_digest(
218 target_bytes: &[u8],
219 method: &str,
220 arguments: &[u8],
221) -> ArtifactChecksumRecord {
222 let mut bytes = EvidenceDigest::new(b"ic-backup/ic-management-request/v1\0");
223 bytes.byte(0); bytes.byte(target_bytes.len().to_le_bytes()[0]); bytes.append(target_bytes);
226 bytes.byte(1); bytes.byte(method.len().to_le_bytes()[0]); bytes.append(method.as_bytes());
229 append_argument_length(&mut bytes, arguments.len());
230 bytes.append(arguments);
231 bytes.finish()
232}
233
234#[expect(
235 clippy::cast_possible_truncation,
236 reason = "admitted argument byte length is at most 4096"
237)]
238fn append_argument_length(bytes: &mut EvidenceDigest, length: usize) {
239 bytes.append(&(length as u32).to_be_bytes());
240}
241
242fn required_snapshot<'de, D: Deserializer<'de>>(
243 deserializer: D,
244) -> Result<Option<Vec<u8>>, D::Error> {
245 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
246}
247struct SnapshotBytes(Vec<u8>);
248impl<'de> Deserialize<'de> for SnapshotBytes {
249 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250 bounded_sequence::deserialize::<_, u8, MAX_IC_SNAPSHOT_ID_BYTES>(
251 deserializer,
252 "at most 256 exact snapshot bytes",
253 IcRequestError::InvalidSnapshotId,
254 )
255 .map(Self)
256 }
257}
258
259#[derive(Debug, Error, Eq, PartialEq)]
261pub enum IcRequestError {
262 #[error("unsupported IC request version {0}")]
264 UnsupportedVersion(u16),
265 #[error("invalid IC request target principal")]
267 InvalidTarget,
268 #[error("load snapshot request requires snapshot_id")]
270 SnapshotRequired,
271 #[error("snapshot_id is not admitted for this IC request method")]
273 UnexpectedSnapshot,
274 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
276 InvalidSnapshotId,
277 #[error("IC request Candid encoding failed: {0}")]
279 Encoding(String),
280 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
282 ArgumentsTooLarge,
283 #[error("IC request must have effect class {expected:?}")]
285 EffectMismatch {
286 expected: IcRequestEffect,
288 },
289 #[error("IC request target differs from original binding")]
291 TargetMismatch,
292 #[error("IC request digest differs from original binding")]
294 DigestMismatch,
295}
296
297#[cfg(test)]
298mod tests;