greentic-component 0.5.2

High-level component loader and store for Greentic components
Documentation
use anyhow::{Context, Result, anyhow, bail};
use greentic_types::cbor::canonical;
use greentic_types::component::ComponentOperation;
use greentic_types::flow::FlowKind;
use greentic_types::{SecretRequirement, cbor::canonical::CanonicalError};
use serde::{Deserialize, Serialize};
use wasm_encoder::{CustomSection, Encode, Section};
use wasmparser::{Parser, Payload};

use crate::capabilities::{Capabilities, ComponentConfigurators, ComponentProfiles};
use crate::limits::Limits;
use crate::manifest::ComponentManifest;
use crate::provenance::Provenance;
use crate::telemetry::TelemetrySpec;

pub const EMBEDDED_COMPONENT_MANIFEST_SECTION_V1: &str = "greentic.component.manifest.v1";
pub const EMBEDDED_COMPONENT_MANIFEST_KIND_V1: &str = "greentic.component.manifest";
pub const EMBEDDED_COMPONENT_MANIFEST_PAYLOAD_SCHEMA_V1: &str = "greentic.component.manifest.v1";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddedComponentDescriptorEnvelopeV1 {
    pub kind: String,
    pub version: u32,
    pub encoding: String,
    pub payload_schema: Option<String>,
    pub payload_hash_blake3: String,
    pub payload: Vec<u8>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddedComponentManifestV1 {
    pub id: String,
    pub name: String,
    pub version: String,
    pub supports: Vec<FlowKind>,
    pub world: String,
    pub capabilities: Capabilities,
    pub secret_requirements: Vec<SecretRequirement>,
    pub profiles: ComponentProfiles,
    pub configurators: Option<ComponentConfigurators>,
    pub limits: Option<Limits>,
    pub telemetry: Option<TelemetrySpec>,
    pub describe_export: String,
    pub operations: Vec<ComponentOperation>,
    pub default_operation: Option<String>,
    pub provenance: Option<Provenance>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct VerifiedEmbeddedDescriptorV1 {
    pub envelope: EmbeddedComponentDescriptorEnvelopeV1,
    pub manifest: EmbeddedComponentManifestV1,
    pub payload_bytes: Vec<u8>,
}

impl EmbeddedComponentManifestV1 {
    pub fn from_canonical_manifest(manifest: &ComponentManifest) -> Self {
        Self {
            id: manifest.id.as_str().to_string(),
            name: manifest.name.clone(),
            version: manifest.version.to_string(),
            supports: manifest.supports.clone(),
            world: manifest.world.as_str().to_string(),
            capabilities: manifest.capabilities.clone(),
            secret_requirements: manifest.secret_requirements.clone(),
            profiles: manifest.profiles.clone(),
            configurators: manifest.configurators.clone(),
            limits: manifest.limits.clone(),
            telemetry: manifest.telemetry.clone(),
            describe_export: manifest.describe_export.as_str().to_string(),
            operations: manifest.operations.clone(),
            default_operation: manifest.default_operation.clone(),
            provenance: manifest.provenance.clone(),
        }
    }
}

pub fn build_embedded_manifest_projection(
    manifest: &ComponentManifest,
) -> EmbeddedComponentManifestV1 {
    EmbeddedComponentManifestV1::from_canonical_manifest(manifest)
}

pub fn encode_embedded_component_descriptor_v1(
    manifest: &EmbeddedComponentManifestV1,
) -> Result<(Vec<u8>, EmbeddedComponentDescriptorEnvelopeV1, Vec<u8>)> {
    let payload = canonical::to_canonical_cbor_allow_floats(manifest)
        .map_err(|err| anyhow!("failed to encode embedded manifest payload: {err}"))?;
    let payload_hash_blake3 = blake3::hash(&payload).to_hex().to_string();
    let envelope = EmbeddedComponentDescriptorEnvelopeV1 {
        kind: EMBEDDED_COMPONENT_MANIFEST_KIND_V1.to_string(),
        version: 1,
        encoding: "application/cbor".to_string(),
        payload_schema: Some(EMBEDDED_COMPONENT_MANIFEST_PAYLOAD_SCHEMA_V1.to_string()),
        payload_hash_blake3,
        payload: payload.clone(),
    };
    let envelope_bytes = canonical::to_canonical_cbor_allow_floats(&envelope)
        .map_err(|err| anyhow!("failed to encode embedded manifest envelope: {err}"))?;
    Ok((envelope_bytes, envelope, payload))
}

pub fn decode_embedded_component_descriptor_v1(
    envelope_bytes: &[u8],
) -> Result<VerifiedEmbeddedDescriptorV1> {
    let envelope: EmbeddedComponentDescriptorEnvelopeV1 = canonical::from_cbor(envelope_bytes)
        .map_err(|err| anyhow!("failed to decode embedded manifest envelope: {err}"))?;
    verify_embedded_component_descriptor_v1(&envelope)
}

pub fn verify_embedded_component_descriptor_v1(
    envelope: &EmbeddedComponentDescriptorEnvelopeV1,
) -> Result<VerifiedEmbeddedDescriptorV1> {
    if envelope.kind != EMBEDDED_COMPONENT_MANIFEST_KIND_V1 {
        bail!("unexpected embedded manifest kind `{}`", envelope.kind);
    }
    if envelope.version != 1 {
        bail!(
            "unsupported embedded manifest version `{}`",
            envelope.version
        );
    }
    if envelope.encoding != "application/cbor" {
        bail!(
            "unsupported embedded manifest encoding `{}`",
            envelope.encoding
        );
    }
    let payload_hash = blake3::hash(&envelope.payload).to_hex().to_string();
    if payload_hash != envelope.payload_hash_blake3 {
        bail!(
            "embedded manifest payload hash mismatch: expected {}, found {}",
            envelope.payload_hash_blake3,
            payload_hash
        );
    }
    let canonical_payload =
        canonical::canonicalize_allow_floats(&envelope.payload).map_err(map_canonical_error)?;
    if canonical_payload != envelope.payload {
        bail!("embedded manifest payload is not canonical");
    }
    let manifest: EmbeddedComponentManifestV1 =
        canonical::from_cbor(&canonical_payload).map_err(map_canonical_error)?;
    Ok(VerifiedEmbeddedDescriptorV1 {
        envelope: envelope.clone(),
        manifest,
        payload_bytes: canonical_payload,
    })
}

pub fn append_embedded_component_manifest_section_v1(
    wasm_bytes: &[u8],
    envelope_bytes: &[u8],
) -> Vec<u8> {
    let mut output = wasm_bytes.to_vec();
    let section = CustomSection {
        name: EMBEDDED_COMPONENT_MANIFEST_SECTION_V1.into(),
        data: envelope_bytes.into(),
    };
    output.push(section.id());
    section.encode(&mut output);
    output
}

pub fn read_embedded_component_manifest_section_v1(wasm_bytes: &[u8]) -> Result<Option<Vec<u8>>> {
    for payload in Parser::new(0).parse_all(wasm_bytes) {
        let payload = payload.map_err(|err| anyhow!("failed to parse wasm: {err}"))?;
        if let Payload::CustomSection(section) = payload
            && section.name() == EMBEDDED_COMPONENT_MANIFEST_SECTION_V1
        {
            return Ok(Some(section.data().to_vec()));
        }
    }
    Ok(None)
}

pub fn read_and_verify_embedded_component_manifest_section_v1(
    wasm_bytes: &[u8],
) -> Result<Option<VerifiedEmbeddedDescriptorV1>> {
    let Some(section) = read_embedded_component_manifest_section_v1(wasm_bytes)? else {
        return Ok(None);
    };
    decode_embedded_component_descriptor_v1(&section).map(Some)
}

fn map_canonical_error(err: CanonicalError) -> anyhow::Error {
    anyhow!(err.to_string())
}

pub fn verify_embedded_projection_matches_canonical_manifest(
    projection: &EmbeddedComponentManifestV1,
    canonical_manifest: &ComponentManifest,
) -> Result<()> {
    let expected = build_embedded_manifest_projection(canonical_manifest);
    if projection != &expected {
        bail!("embedded manifest projection does not match canonical build-time manifest");
    }
    Ok(())
}

pub fn embed_and_verify_wasm(
    wasm_path: &std::path::Path,
    canonical_manifest: &ComponentManifest,
) -> Result<()> {
    let wasm_bytes = std::fs::read(wasm_path)
        .with_context(|| format!("failed to read wasm at {}", wasm_path.display()))?;
    let projection = build_embedded_manifest_projection(canonical_manifest);
    let (envelope_bytes, _envelope, _payload_bytes) =
        encode_embedded_component_descriptor_v1(&projection)?;
    let patched = append_embedded_component_manifest_section_v1(&wasm_bytes, &envelope_bytes);
    std::fs::write(wasm_path, &patched).with_context(|| {
        format!(
            "failed to write embedded manifest to {}",
            wasm_path.display()
        )
    })?;

    let verified = read_and_verify_embedded_component_manifest_section_v1(&patched)?
        .ok_or_else(|| anyhow!("embedded manifest section missing after write"))?;
    verify_embedded_projection_matches_canonical_manifest(&verified.manifest, canonical_manifest)?;

    let section_bytes = read_embedded_component_manifest_section_v1(&patched)?
        .ok_or_else(|| anyhow!("embedded manifest section missing after verification"))?;
    if section_bytes != envelope_bytes {
        bail!("embedded manifest envelope bytes changed during write/read verification");
    }
    Ok(())
}