gatehouse_runtime_protocol/lifecycle_v2/
plan_record.rs1use super::{
3 native_image::{decode_image, image_route},
4 ownership::FixedPort,
5 profile::{Profile, SlotIdentity},
6 protected_context::ProtectedContext,
7 readonly::CapturedImage,
8};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11use std::collections::BTreeMap;
12type Result<T> = std::result::Result<T, &'static str>;
13const SCHEMA: &str = "gate-runtime/podman-plan-evidence-v1";
14const LIMIT: usize = 16 * 1024 * 1024;
15#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct PlanInputs {
18 pub name: String,
19 pub labels: BTreeMap<String, String>,
20 pub port: Option<PlanPort>,
21}
22impl PlanInputs {
23 pub fn validate(&self) -> Result<()> {
26 super::ownership::validate_plan_fields(
27 &self.name,
28 &self.labels,
29 self.port
30 .as_ref()
31 .map(|p| FixedPort {
32 container: p.container,
33 host: p.host,
34 })
35 .as_ref(),
36 false,
37 )
38 }
39}
40#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct PlanPort {
43 pub container: u16,
44 pub host: u16,
45}
46#[derive(Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48struct Wire {
49 schema: String,
50 profile: Profile,
51 asserted_installation_sha256: String,
52 asserted_executable_sha256: String,
53 config_json: String,
54 config_sha256: String,
55 operator_epoch: String,
56 image_id: String,
57 image_json: String,
58 image_sha256: String,
59 inputs: PlanInputs,
60 plan_json: String,
61 plan_sha256: String,
62}
63pub struct PlanRecord {
65 wire: Wire,
66}
67fn hash(bytes: &[u8]) -> String {
68 format!("{:x}", Sha256::digest(bytes))
69}
70impl PlanRecord {
71 pub fn capture(
72 context: &ProtectedContext,
73 captured: &CapturedImage,
74 identity: &SlotIdentity,
75 inputs: PlanInputs,
76 ) -> Result<Self> {
77 inputs.validate()?;
78 captured.match_context(context)?;
79 identity.validate()?;
80 if identity.config_sha256 != hash(captured.config_bytes()) {
81 return Err("slot config mismatch");
82 }
83 let plan = derive(captured.facts(), &inputs)?;
84 let wire = Wire {
85 schema: SCHEMA.into(),
86 profile: identity.profile.clone(),
87 asserted_installation_sha256: identity.installation_sha256.clone(),
88 asserted_executable_sha256: identity.executable_sha256.clone(),
89 config_json: std::str::from_utf8(captured.config_bytes())
90 .map_err(|_| "config UTF8")?
91 .into(),
92 config_sha256: identity.config_sha256.clone(),
93 operator_epoch: captured.operator_epoch().into(),
94 image_id: captured.facts().id().into(),
95 image_json: std::str::from_utf8(captured.image_bytes())
96 .map_err(|_| "image UTF8")?
97 .into(),
98 image_sha256: hash(captured.image_bytes()),
99 inputs,
100 plan_sha256: hash(plan.as_bytes()),
101 plan_json: plan,
102 };
103 let record = Self { wire };
104 record.validate(context)?;
105 Ok(record)
106 }
107 pub fn encode(&self) -> Result<Vec<u8>> {
108 let bytes = serde_json::to_vec(&self.wire).map_err(|_| "record encoding")?;
109 if bytes.len() > LIMIT {
110 return Err("record too large");
111 }
112 Ok(bytes)
113 }
114 pub fn decode(bytes: &[u8], context: &ProtectedContext) -> Result<Self> {
115 if bytes.is_empty() || bytes.len() > LIMIT {
116 return Err("record size");
117 }
118 let wire = serde_json::from_slice(bytes).map_err(|_| "record fields")?;
119 let record = Self { wire };
120 if record.encode()? != bytes {
121 return Err("noncanonical record");
122 }
123 record.validate(context)?;
124 Ok(record)
125 }
126 pub fn match_capture_request(&self, image_id: &str, inputs: &PlanInputs) -> Result<()> {
129 super::native_image::image_route(image_id)?;
130 inputs.validate()?;
131 if self.wire.image_id != image_id || self.wire.inputs != *inputs {
132 return Err("capture record request mismatch");
133 }
134 Ok(())
135 }
136 pub(super) fn match_installation_snapshot(
137 &self,
138 identity: &SlotIdentity,
139 config: &[u8],
140 ) -> Result<()> {
141 identity.validate()?;
142 if self.wire.profile != identity.profile
143 || self.wire.asserted_installation_sha256 != identity.installation_sha256
144 || self.wire.asserted_executable_sha256 != identity.executable_sha256
145 || self.wire.config_sha256 != identity.config_sha256
146 || self.wire.config_json.as_bytes() != config
147 {
148 return Err("record installation snapshot mismatch");
149 }
150 Ok(())
151 }
152 pub fn plan_bytes(&self) -> &[u8] {
153 self.wire.plan_json.as_bytes()
154 }
155 pub fn installation_verified(&self) -> bool {
156 false
157 }
158 fn validate(&self, context: &ProtectedContext) -> Result<()> {
159 let w = &self.wire;
160 let (expected, bytes) = context.snapshot()?;
161 if w.schema != SCHEMA
162 || w.config_json.len() > 1024 * 1024
163 || w.image_json.len() > 1024 * 1024
164 || bytes != w.config_json.as_bytes()
165 || expected.operator_epoch != w.operator_epoch
166 {
167 return Err("record context mismatch");
168 }
169 SlotIdentity {
170 profile: w.profile.clone(),
171 installation_sha256: w.asserted_installation_sha256.clone(),
172 executable_sha256: w.asserted_executable_sha256.clone(),
173 config_sha256: w.config_sha256.clone(),
174 }
175 .validate()?;
176 if hash(bytes) != w.config_sha256
177 || hash(w.image_json.as_bytes()) != w.image_sha256
178 || hash(w.plan_json.as_bytes()) != w.plan_sha256
179 {
180 return Err("record digest mismatch");
181 }
182 let facts = decode_image(
183 &w.image_id,
184 "GET",
185 &image_route(&w.image_id)?,
186 200,
187 w.image_json.as_bytes(),
188 )?;
189 if derive(&facts, &w.inputs)? != w.plan_json {
190 return Err("record plan mismatch");
191 }
192 context.revalidate()
193 }
194}
195fn derive(facts: &super::native_image::ImageFacts, inputs: &PlanInputs) -> Result<String> {
196 inputs.validate()?;
197 let plan = facts.create_plan(
198 inputs.name.clone(),
199 inputs.labels.clone(),
200 inputs.port.as_ref().map(|p| FixedPort {
201 container: p.container,
202 host: p.host,
203 }),
204 )?;
205 String::from_utf8(plan.body()?).map_err(|_| "plan UTF8")
206}