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