Skip to main content

greentic_component/
embedded_descriptor.rs

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(&section).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}