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 !admits_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 encode_arguments(TakeCanisterSnapshotArgs {
101 canister_id: principal.into(),
102 replace_snapshot: None,
103 uninstall_code: Some(false),
104 sender_canister_version: None,
105 })
106 }
107 IcManagementMethodRecord::LoadCanisterSnapshot => {
108 encode_arguments(LoadCanisterSnapshotArgs {
109 canister_id: principal.into(),
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 => encode_arguments(CanisterIdRecord {
121 canister_id: principal.into(),
122 }),
123 }?;
124 Ok(Self {
125 version: 1,
126 method: request.method,
127 target,
128 snapshot_id: request.snapshot_id,
129 target_bytes: principal.as_slice().to_vec(),
130 arguments,
131 })
132 }
133 #[must_use]
135 pub const fn method(&self) -> IcManagementMethodRecord {
136 self.method
137 }
138 #[must_use]
140 pub fn target(&self) -> &str {
141 &self.target
142 }
143 #[must_use]
145 pub fn snapshot_id(&self) -> Option<&[u8]> {
146 self.snapshot_id.as_deref()
147 }
148 #[must_use]
150 pub fn arguments(&self) -> &[u8] {
151 &self.arguments
152 }
153 #[must_use]
155 pub const fn receiver(&self) -> &'static str {
156 "aaaaa-aa"
157 }
158 #[must_use]
163 pub fn digest(&self) -> ArtifactChecksumRecord {
164 management_request_digest(&self.target_bytes, self.method.name(), &self.arguments)
165 }
166 pub fn validate_mutation_binding(
172 &self,
173 binding: &OperationBindingRecord,
174 ) -> Result<(), IcRequestError> {
175 self.require_effect(IcRequestEffect::Mutation)?;
176 self.validate_identity(binding, binding.request())
177 }
178 pub fn validate_observation_binding(
185 &self,
186 binding: &OperationBindingRecord,
187 request: &ArtifactChecksumRecord,
188 ) -> Result<(), IcRequestError> {
189 self.require_effect(IcRequestEffect::Observation)?;
190 self.validate_identity(binding, request.hash())
191 }
192 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
193 if self.method.effect() != expected {
194 return Err(IcRequestError::EffectMismatch { expected });
195 }
196 Ok(())
197 }
198 fn validate_identity(
199 &self,
200 binding: &OperationBindingRecord,
201 expected: &str,
202 ) -> Result<(), IcRequestError> {
203 if self.target != binding.target() {
204 return Err(IcRequestError::TargetMismatch);
205 }
206 if self.digest().hash() != expected {
207 return Err(IcRequestError::DigestMismatch);
208 }
209 Ok(())
210 }
211}
212
213pub(super) fn encode_arguments(
214 value: impl contract::CandidType,
215) -> Result<Vec<u8>, IcRequestError> {
216 let arguments =
217 contract::encode_one(value).map_err(|error| IcRequestError::Encoding(error.to_string()))?;
218 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
219 return Err(IcRequestError::ArgumentsTooLarge);
220 }
221 Ok(arguments)
222}
223
224pub(super) fn admits_snapshot_id(bytes: &[u8]) -> bool {
227 !bytes.is_empty() && bytes.len() <= MAX_IC_SNAPSHOT_ID_BYTES
228}
229
230pub(super) fn management_request_digest(
232 target_bytes: &[u8],
233 method: &str,
234 arguments: &[u8],
235) -> ArtifactChecksumRecord {
236 let mut bytes = EvidenceDigest::new(b"ic-backup/ic-management-request/v1\0");
237 bytes.byte(0); bytes.byte(target_bytes.len().to_le_bytes()[0]); bytes.append(target_bytes);
240 bytes.byte(1); bytes.byte(method.len().to_le_bytes()[0]); bytes.append(method.as_bytes());
243 append_argument_length(&mut bytes, arguments.len());
244 bytes.append(arguments);
245 bytes.finish()
246}
247
248#[expect(
249 clippy::cast_possible_truncation,
250 reason = "admitted argument byte length is at most 4096"
251)]
252fn append_argument_length(bytes: &mut EvidenceDigest, length: usize) {
253 bytes.append(&(length as u32).to_be_bytes());
254}
255
256fn required_snapshot<'de, D: Deserializer<'de>>(
257 deserializer: D,
258) -> Result<Option<Vec<u8>>, D::Error> {
259 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
260}
261struct SnapshotBytes(Vec<u8>);
262impl<'de> Deserialize<'de> for SnapshotBytes {
263 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
264 bounded_sequence::deserialize::<_, u8, MAX_IC_SNAPSHOT_ID_BYTES>(
265 deserializer,
266 "at most 256 exact snapshot bytes",
267 IcRequestError::InvalidSnapshotId,
268 )
269 .map(Self)
270 }
271}
272
273#[derive(Debug, Error, Eq, PartialEq)]
275pub enum IcRequestError {
276 #[error("unsupported IC request version {0}")]
278 UnsupportedVersion(u16),
279 #[error("invalid IC request target principal")]
281 InvalidTarget,
282 #[error("load snapshot request requires snapshot_id")]
284 SnapshotRequired,
285 #[error("snapshot_id is not admitted for this IC request method")]
287 UnexpectedSnapshot,
288 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
290 InvalidSnapshotId,
291 #[error("IC request Candid encoding failed: {0}")]
293 Encoding(String),
294 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
296 ArgumentsTooLarge,
297 #[error("IC request must have effect class {expected:?}")]
299 EffectMismatch {
300 expected: IcRequestEffect,
302 },
303 #[error("IC request target differs from original binding")]
305 TargetMismatch,
306 #[error("IC request digest differs from original binding")]
308 DigestMismatch,
309}
310
311#[cfg(test)]
312mod tests;