1use anyhow::{Context, Result, anyhow, bail};
2use greentic_types::cbor::canonical;
3use greentic_types::component::ComponentOperation;
4use greentic_types::flow::FlowKind;
5use greentic_types::{SecretRequirement, cbor::canonical::CanonicalError};
6use serde::{Deserialize, Serialize};
7use wasm_encoder::{CustomSection, Encode, Section};
8use wasmparser::{Parser, Payload};
9
10use crate::capabilities::{Capabilities, ComponentConfigurators, ComponentProfiles};
11use crate::limits::Limits;
12use crate::manifest::ComponentManifest;
13use crate::provenance::Provenance;
14use crate::telemetry::TelemetrySpec;
15
16pub const EMBEDDED_COMPONENT_MANIFEST_SECTION_V1: &str = "greentic.component.manifest.v1";
17pub const EMBEDDED_COMPONENT_MANIFEST_KIND_V1: &str = "greentic.component.manifest";
18pub const EMBEDDED_COMPONENT_MANIFEST_PAYLOAD_SCHEMA_V1: &str = "greentic.component.manifest.v1";
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct EmbeddedComponentDescriptorEnvelopeV1 {
22 pub kind: String,
23 pub version: u32,
24 pub encoding: String,
25 pub payload_schema: Option<String>,
26 pub payload_hash_blake3: String,
27 pub payload: Vec<u8>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct EmbeddedComponentManifestV1 {
32 pub id: String,
33 pub name: String,
34 pub version: String,
35 pub supports: Vec<FlowKind>,
36 pub world: String,
37 pub capabilities: Capabilities,
38 pub secret_requirements: Vec<SecretRequirement>,
39 pub profiles: ComponentProfiles,
40 pub configurators: Option<ComponentConfigurators>,
41 pub limits: Option<Limits>,
42 pub telemetry: Option<TelemetrySpec>,
43 pub describe_export: String,
44 pub operations: Vec<ComponentOperation>,
45 pub default_operation: Option<String>,
46 pub provenance: Option<Provenance>,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub struct VerifiedEmbeddedDescriptorV1 {
51 pub envelope: EmbeddedComponentDescriptorEnvelopeV1,
52 pub manifest: EmbeddedComponentManifestV1,
53 pub payload_bytes: Vec<u8>,
54}
55
56impl EmbeddedComponentManifestV1 {
57 pub fn from_canonical_manifest(manifest: &ComponentManifest) -> Self {
58 Self {
59 id: manifest.id.as_str().to_string(),
60 name: manifest.name.clone(),
61 version: manifest.version.to_string(),
62 supports: manifest.supports.clone(),
63 world: manifest.world.as_str().to_string(),
64 capabilities: manifest.capabilities.clone(),
65 secret_requirements: manifest.secret_requirements.clone(),
66 profiles: manifest.profiles.clone(),
67 configurators: manifest.configurators.clone(),
68 limits: manifest.limits.clone(),
69 telemetry: manifest.telemetry.clone(),
70 describe_export: manifest.describe_export.as_str().to_string(),
71 operations: manifest.operations.clone(),
72 default_operation: manifest.default_operation.clone(),
73 provenance: manifest.provenance.clone(),
74 }
75 }
76}
77
78pub fn build_embedded_manifest_projection(
79 manifest: &ComponentManifest,
80) -> EmbeddedComponentManifestV1 {
81 EmbeddedComponentManifestV1::from_canonical_manifest(manifest)
82}
83
84pub fn encode_embedded_component_descriptor_v1(
85 manifest: &EmbeddedComponentManifestV1,
86) -> Result<(Vec<u8>, EmbeddedComponentDescriptorEnvelopeV1, Vec<u8>)> {
87 let payload = canonical::to_canonical_cbor_allow_floats(manifest)
88 .map_err(|err| anyhow!("failed to encode embedded manifest payload: {err}"))?;
89 let payload_hash_blake3 = blake3::hash(&payload).to_hex().to_string();
90 let envelope = EmbeddedComponentDescriptorEnvelopeV1 {
91 kind: EMBEDDED_COMPONENT_MANIFEST_KIND_V1.to_string(),
92 version: 1,
93 encoding: "application/cbor".to_string(),
94 payload_schema: Some(EMBEDDED_COMPONENT_MANIFEST_PAYLOAD_SCHEMA_V1.to_string()),
95 payload_hash_blake3,
96 payload: payload.clone(),
97 };
98 let envelope_bytes = canonical::to_canonical_cbor_allow_floats(&envelope)
99 .map_err(|err| anyhow!("failed to encode embedded manifest envelope: {err}"))?;
100 Ok((envelope_bytes, envelope, payload))
101}
102
103pub fn decode_embedded_component_descriptor_v1(
104 envelope_bytes: &[u8],
105) -> Result<VerifiedEmbeddedDescriptorV1> {
106 let envelope: EmbeddedComponentDescriptorEnvelopeV1 = canonical::from_cbor(envelope_bytes)
107 .map_err(|err| anyhow!("failed to decode embedded manifest envelope: {err}"))?;
108 verify_embedded_component_descriptor_v1(&envelope)
109}
110
111pub fn verify_embedded_component_descriptor_v1(
112 envelope: &EmbeddedComponentDescriptorEnvelopeV1,
113) -> Result<VerifiedEmbeddedDescriptorV1> {
114 if envelope.kind != EMBEDDED_COMPONENT_MANIFEST_KIND_V1 {
115 bail!("unexpected embedded manifest kind `{}`", envelope.kind);
116 }
117 if envelope.version != 1 {
118 bail!(
119 "unsupported embedded manifest version `{}`",
120 envelope.version
121 );
122 }
123 if envelope.encoding != "application/cbor" {
124 bail!(
125 "unsupported embedded manifest encoding `{}`",
126 envelope.encoding
127 );
128 }
129 let payload_hash = blake3::hash(&envelope.payload).to_hex().to_string();
130 if payload_hash != envelope.payload_hash_blake3 {
131 bail!(
132 "embedded manifest payload hash mismatch: expected {}, found {}",
133 envelope.payload_hash_blake3,
134 payload_hash
135 );
136 }
137 let canonical_payload =
138 canonical::canonicalize_allow_floats(&envelope.payload).map_err(map_canonical_error)?;
139 if canonical_payload != envelope.payload {
140 bail!("embedded manifest payload is not canonical");
141 }
142 let manifest: EmbeddedComponentManifestV1 =
143 canonical::from_cbor(&canonical_payload).map_err(map_canonical_error)?;
144 Ok(VerifiedEmbeddedDescriptorV1 {
145 envelope: envelope.clone(),
146 manifest,
147 payload_bytes: canonical_payload,
148 })
149}
150
151pub fn append_embedded_component_manifest_section_v1(
152 wasm_bytes: &[u8],
153 envelope_bytes: &[u8],
154) -> Vec<u8> {
155 let mut output = wasm_bytes.to_vec();
156 let section = CustomSection {
157 name: EMBEDDED_COMPONENT_MANIFEST_SECTION_V1.into(),
158 data: envelope_bytes.into(),
159 };
160 output.push(section.id());
161 section.encode(&mut output);
162 output
163}
164
165pub fn read_embedded_component_manifest_section_v1(wasm_bytes: &[u8]) -> Result<Option<Vec<u8>>> {
166 for payload in Parser::new(0).parse_all(wasm_bytes) {
167 let payload = payload.map_err(|err| anyhow!("failed to parse wasm: {err}"))?;
168 if let Payload::CustomSection(section) = payload
169 && section.name() == EMBEDDED_COMPONENT_MANIFEST_SECTION_V1
170 {
171 return Ok(Some(section.data().to_vec()));
172 }
173 }
174 Ok(None)
175}
176
177pub fn read_and_verify_embedded_component_manifest_section_v1(
178 wasm_bytes: &[u8],
179) -> Result<Option<VerifiedEmbeddedDescriptorV1>> {
180 let Some(section) = read_embedded_component_manifest_section_v1(wasm_bytes)? else {
181 return Ok(None);
182 };
183 decode_embedded_component_descriptor_v1(§ion).map(Some)
184}
185
186fn map_canonical_error(err: CanonicalError) -> anyhow::Error {
187 anyhow!(err.to_string())
188}
189
190pub fn verify_embedded_projection_matches_canonical_manifest(
191 projection: &EmbeddedComponentManifestV1,
192 canonical_manifest: &ComponentManifest,
193) -> Result<()> {
194 let expected = build_embedded_manifest_projection(canonical_manifest);
195 if projection != &expected {
196 bail!("embedded manifest projection does not match canonical build-time manifest");
197 }
198 Ok(())
199}
200
201pub fn embed_and_verify_wasm(
202 wasm_path: &std::path::Path,
203 canonical_manifest: &ComponentManifest,
204) -> Result<()> {
205 let wasm_bytes = std::fs::read(wasm_path)
206 .with_context(|| format!("failed to read wasm at {}", wasm_path.display()))?;
207 let projection = build_embedded_manifest_projection(canonical_manifest);
208 let (envelope_bytes, _envelope, _payload_bytes) =
209 encode_embedded_component_descriptor_v1(&projection)?;
210 let patched = append_embedded_component_manifest_section_v1(&wasm_bytes, &envelope_bytes);
211 std::fs::write(wasm_path, &patched).with_context(|| {
212 format!(
213 "failed to write embedded manifest to {}",
214 wasm_path.display()
215 )
216 })?;
217
218 let verified = read_and_verify_embedded_component_manifest_section_v1(&patched)?
219 .ok_or_else(|| anyhow!("embedded manifest section missing after write"))?;
220 verify_embedded_projection_matches_canonical_manifest(&verified.manifest, canonical_manifest)?;
221
222 let section_bytes = read_embedded_component_manifest_section_v1(&patched)?
223 .ok_or_else(|| anyhow!("embedded manifest section missing after verification"))?;
224 if section_bytes != envelope_bytes {
225 bail!("embedded manifest envelope bytes changed during write/read verification");
226 }
227 Ok(())
228}