Skip to content

Fix/oci multi layer support#217

Merged
Mossaka merged 2 commits into
microsoft:mainfrom
attackordie:fix/oci-multi-layer-support
Sep 17, 2025
Merged

Fix/oci multi layer support#217
Mossaka merged 2 commits into
microsoft:mainfrom
attackordie:fix/oci-multi-layer-support

Conversation

@attackordie

Copy link
Copy Markdown
Contributor

Fix OCI multi-layer support for components with policies

Summary

This PR implements full OCI Distribution Specification v1.1 compliance by adding support for multi-layer OCI artifacts. Components can now be distributed with their security policies as a single atomic OCI artifact, fixing a critical bug where policies weren't automatically attached during dynamic component loading.

Fixes #216

Problems Fixed

This PR addresses multiple issues:

  1. OCI Multi-layer Support - The OCI loader only extracted the first layer from OCI artifacts, ignoring additional layers like security policies (due to limitations in the oci-wasm crate)

  2. Dynamic Loading Bug - Policies from OCI artifacts weren't automatically attached when components were dynamically loaded at runtime, though they worked correctly at startup

  3. Missing Digest Verification - No integrity checking was performed on downloaded OCI layers, creating a security vulnerability

  4. Registry Timeout Issues - OCI operations could hang indefinitely on slow or unresponsive registries

  5. CNCF WebAssembly OCI Spec Compliance - Didn't fully support the emerging WebAssembly OCI artifact specifications

  6. Poor Error Messages - Users received cryptic "Incompatible layer media type" errors without understanding the root cause

Solution

  1. New oci_multi_layer module that:

    • Directly uses oci-client to fetch manifests and blobs
    • Processes each layer based on its media type
    • Verifies SHA256 digests for integrity checking
    • Implements 30-second timeout to prevent hanging
    • Maintains backward compatibility with single-layer artifacts
    • Provides clear error messages for multi-layer detection
  2. Fixed policy auto-attachment in LifecycleManager::load_component:

    • Now checks for co-located policy files after copying to plugin directory
    • Automatically attaches policies during runtime loading (not just startup)
    • Ensures consistent behavior between startup and dynamic loading
  3. CNCF WebAssembly OCI compliance:

    • Supports both application/vnd.wasm.config.v0+json and application/vnd.wasm.config.v1+json
    • Handles wasip1 and wasip2 configurations
    • Ready for future media types (signatures, attestations, SBOMs)

Changes

New Files

  • crates/wassette/src/oci_multi_layer.rs - Core multi-layer OCI support implementation
  • tests/oci_multi_layer_test.rs - Integration tests for multi-layer artifacts
  • tests/oci_cncf_spec_test.rs - CNCF WebAssembly OCI artifact spec compliance tests
  • tests/qr_generator_integration_test.rs - Real-world multi-layer component tests

Modified Files

  • crates/wassette/src/loader.rs - Updated to use new multi-layer loader
  • crates/wassette/src/lib.rs - Added module declaration and fixed policy auto-attachment bug
  • .gitignore - Added patterns for test artifacts

Features Implemented

Full multi-layer OCI artifact support - Processes all layers based on media type
Automatic policy extraction - Policy layers are automatically detected and attached
SHA256 digest verification - Each layer's integrity is verified
Backward compatibility - Single-layer WASM artifacts continue to work
Timeout configuration - 30-second timeout prevents hanging on registry operations

Supported Media Types

  • application/wasm - WebAssembly components
  • application/vnd.wassette.policy+yaml - Security policies
  • Future ready for:
    • application/vnd.dev.cosign.simplesigning.v1+json - Cosign signatures
    • application/vnd.cncf.notary.signature - Notation signatures
    • application/vnd.in-toto+json - In-toto attestations

Installation & Testing

Building from Source

# Clone and checkout this branch
git clone https://github.com/microsoft/wassette.git
cd wassette
git checkout fix/oci-multi-layer-support

# Build in release mode (takes ~3-5 minutes)
cargo build --release --bin wassette

# Verify the build
./target/release/wassette --version
# Expected output: wassette-mcp-server 0.2.0 ... GitTag:"v0.2.0-72-g9c32131"

Installing the Binary

# Option 1: Install to user directory (~/.local/bin)
mkdir -p ~/.local/bin
cp ./target/release/wassette ~/.local/bin/wassette

# Option 2: Use cargo install
cargo install --path . --force

# Verify installation
wassette --version

Running Tests

# Run OCI multi-layer specific tests
cargo test --test oci_multi_layer_test -- --include-ignored

# Run CNCF spec compliance tests
cargo test --test oci_cncf_spec_test

# Run QR generator integration tests (real-world example)
cargo test --test qr_generator_integration_test

# Run all workspace tests
cargo test --workspace

Test Results (All Passing ✅)

  • OCI Multi-layer Tests: 3/3 passed
    • test_multi_layer_with_policy_registry - Registry.mcpsearchtool.com support
    • test_single_layer_wasm_compatibility - Backward compatibility
    • test_load_component_with_policy_from_oci - Automatic policy attachment
  • CNCF Spec Tests: 11/11 passed
    • Digest verification, media type validation, config parsing
  • QR Generator Tests: 10/10 passed
    • Real-world multi-layer component with policy
  • All Workspace Tests: 150+ tests passing

Security Enhancements

  • SHA256 Digest Verification: Every OCI layer is verified against its manifest digest before processing, preventing tampering
  • Media Type Validation: Only recognized media types are processed, rejecting potentially malicious content
  • Timeout Protection: 30-second timeout prevents denial-of-service via slow registries
  • Secure Temporary Storage: Components downloaded to secure OS temporary directories with proper permissions
  • Error Information Disclosure: Error messages improved without revealing sensitive system information
  • Backward Compatibility: Existing security model and policies fully preserved
  • Atomic Operations: Policy attachment is atomic - either fully succeeds or fully fails

Usage Example

Before this PR:

# Had to load component and policy separately
wassette load-component oci://registry/component:tag
wassette attach-policy file:///path/to/policy.yaml component-id

After this PR:

# Component and policy loaded atomically from single OCI artifact
wassette load-component oci://registry/component:tag
# Policy automatically attached if present in OCI layers

Real-World Testing Example

With an AI Assistant (Claude, GitHub Copilot, etc.) using Wassette MCP:

  1. Configure your AI assistant to use Wassette MCP server
  2. Load and test the QR generator component through natural language:
User: Please load the component from oci://registry.mcpsearchtool.com/test/qr-generator:latest

AI Assistant: I'll load the QR generator component from the OCI registry.

[Loading component from oci://registry.mcpsearchtool.com/test/qr-generator:latest]
Component loaded successfully: {"id":"test_qr-generator","status":"component loaded"}

The component has been loaded successfully. You now have access to three QR code generation tools:
- generate-qr: Create a QR code with default settings
- generate-qr-custom: Create a QR code with custom size
- save-qr: Generate and save a QR code to a file

User: Please generate a QR code for the Wassette GitHub repository

AI Assistant: I'll generate a QR code for the Wassette GitHub repository URL.

[Generates QR code SVG]
Here's your QR code for https://github.com/microsoft/wassette. The QR code has been generated as an SVG image that encodes the repository URL.

User: Now save a QR code for "Hello Wassette!" to /tmp/hello.svg with size 200

AI Assistant: I'll save a QR code with your text to the specified file.

[Saves QR code to /tmp/hello.svg]
Successfully saved a 200x200 QR code containing "Hello Wassette!" to /tmp/hello.svg

The component and its security policy are loaded atomically from the OCI registry, demonstrating the new multi-layer support.

Impact and Benefits

Immediate Benefits

  • Fixes critical runtime bug: Policies now attach correctly during dynamic loading
  • Improves security: SHA256 verification prevents tampering
  • Better user experience: Clear error messages and timeout protection
  • OCI compliance: Full support for multi-layer artifacts per spec

Enables Future Features

  • Unblocks Add signature or digest verification for OCI images #25: Signature verification can now access signature layers
  • Atomic distribution: Components and policies as single artifacts
  • Supply chain security: Foundation for signed artifacts
  • SBOM support: Software Bill of Materials as additional layers
  • Attestation support: SLSA provenance and in-toto attestations
  • Cosign integration: Ready for application/vnd.dev.cosign.simplesigning.v1+json
  • Notation support: Ready for application/vnd.cncf.notary.signature

Breaking Changes

None. This PR maintains full backward compatibility.

Migration Guide

No migration needed. Existing single-layer OCI artifacts continue to work exactly as before.

Checklist

  • Tests pass locally
  • Code follows project style guidelines
  • Security implications considered
  • Backward compatibility maintained
  • Documentation updated where needed
  • No secrets or keys in code

Related Issues and PRs

@attackordie

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree company="Brainstorm Limitless, LLC"

@Mossaka

Mossaka commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Thanks for contributing. As discussed in the discord server, I will defer the PR review to @thomastaylor312

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements full OCI Distribution Specification v1.1 compliance by adding support for multi-layer OCI artifacts, enabling components to be distributed with security policies as atomic units and fixing critical bugs in policy attachment during dynamic loading.

  • Added multi-layer OCI artifact support with SHA256 digest verification and timeout protection
  • Fixed automatic policy attachment during runtime component loading (not just startup)
  • Implemented CNCF WebAssembly OCI artifact specification compliance with wasip1/wasip2 support

Reviewed Changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
crates/wassette/src/oci_multi_layer.rs Core multi-layer OCI support with digest verification and CNCF spec compliance
crates/wassette/src/loader.rs Updated component loader to handle multi-layer artifacts and copy co-located policies
crates/wassette/src/lib.rs Added timeout configuration and fixed policy auto-attachment during dynamic loading
tests/qr_generator_integration_test.rs Integration tests using real multi-layer QR generator component
tests/oci_multi_layer_test.rs OCI multi-layer functionality tests with registry health checks
tests/oci_digest_verification_test.rs SHA256 digest verification tests for security compliance
tests/oci_cncf_spec_test.rs CNCF WebAssembly OCI artifact specification compliance tests
crates/wassette/Cargo.toml Added hex and sha2 dependencies for digest verification
Cargo.toml Added workspace-level hex and sha2 dependencies

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread tests/qr_generator_integration_test.rs Outdated
Comment on lines +174 to +185
let reference: oci_client::Reference = "registry.mcpsearchtool.com/test/qr-generator:v1755367253"
.parse()?;

let client = oci_client::Client::default();

Copilot AI Aug 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test creates a new OCI client without timeout configuration, which could hang indefinitely. Consider using the same timeout configuration as in the main library (30 seconds) for consistency and to prevent test hangs.

Suggested change
let reference: oci_client::Reference = "registry.mcpsearchtool.com/test/qr-generator:v1755367253"
.parse()?;
let client = oci_client::Client::default();
let client = oci_client::Client::with_timeout(Duration::from_secs(30));

Copilot uses AI. Check for mistakes.
Comment thread tests/oci_digest_verification_test.rs Outdated
Comment thread crates/wassette/src/oci_multi_layer.rs Outdated
Comment on lines +131 to +132
if image_manifest.config.media_type == CONFIG_MEDIA_TYPE
|| image_manifest.config.media_type == "application/vnd.oci.image.config.v1+json" {

Copilot AI Aug 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded media type string should be defined as a constant for consistency and maintainability, similar to how CONFIG_MEDIA_TYPE is defined.

Suggested change
if image_manifest.config.media_type == CONFIG_MEDIA_TYPE
|| image_manifest.config.media_type == "application/vnd.oci.image.config.v1+json" {
|| image_manifest.config.media_type == OCI_IMAGE_CONFIG_MEDIA_TYPE {

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +264
// Check if this is a multi-layer artifact issue
let error_str = e.to_string();
if error_str.contains("Incompatible layer media type") {
// Multi-layer artifact detected - use our custom handler
info!("Multi-layer OCI artifact detected, using direct OCI client");

// Use our new multi-layer support to get ALL layers
let artifact = crate::oci_multi_layer::pull_multi_layer_artifact(&reference, oci_client)
.await
.context("Failed to extract layers from multi-layer OCI artifact")?;

// Save the WASM data
let component_name = reference.repository().replace('/', "_");
let (downloaded_resource, mut file) = DownloadedResource::new_temp_file(
&component_name,
Self::FILE_EXTENSION,
)
.await?;

file.write_all(&artifact.wasm_data).await?;
file.flush().await?;
file.sync_all().await?;
drop(file);

// If there's a policy, save it alongside the WASM in the temp directory
if let Some(policy_data) = artifact.policy_data {
info!("Saving policy layer alongside component");

// Create policy file in the same temp directory as the WASM
if let DownloadedResource::Temp((ref tempdir, ref _wasm_path)) = downloaded_resource {
let policy_path = tempdir.path().join(format!("{}.policy.yaml", component_name));
tokio::fs::write(&policy_path, &policy_data)
.await
.context("Failed to save policy file")?;
info!("Policy saved to: {:?}", policy_path);
}
}

info!("Successfully extracted WASM component and policy from multi-layer artifact");

Ok(downloaded_resource)

Copilot AI Aug 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-based error detection is fragile and could break if the error message format changes. Consider using structured error types or error codes for more reliable error handling.

Note: See the diff below for a potential fix:

@@ -217,47 +218,51 @@
                 Ok(downloaded_resource)
             }
             Err(e) => {
-                // Check if this is a multi-layer artifact issue
-                let error_str = e.to_string();
-                if error_str.contains("Incompatible layer media type") {
-                    // Multi-layer artifact detected - use our custom handler
-                    info!("Multi-layer OCI artifact detected, using direct OCI client");
-                    
-                    // Use our new multi-layer support to get ALL layers
-                    let artifact = crate::oci_multi_layer::pull_multi_layer_artifact(&reference, oci_client)
-                        .await
-                        .context("Failed to extract layers from multi-layer OCI artifact")?;
-                    
-                    // Save the WASM data
-                    let component_name = reference.repository().replace('/', "_");
-                    let (downloaded_resource, mut file) = DownloadedResource::new_temp_file(
-                        &component_name,
-                        Self::FILE_EXTENSION,
-                    )
-                    .await?;
-                    
-                    file.write_all(&artifact.wasm_data).await?;
-                    file.flush().await?;
-                    file.sync_all().await?;
-                    drop(file);
-                    
-                    // If there's a policy, save it alongside the WASM in the temp directory
-                    if let Some(policy_data) = artifact.policy_data {
-                        info!("Saving policy layer alongside component");
+                // Check if this is a multi-layer artifact issue using structured error type
+                if let Some(oci_wasm_error) = e.downcast_ref::<OciWasmError>() {
+                    if let OciWasmError::IncompatibleLayerMediaType = oci_wasm_error {
+                        // Multi-layer artifact detected - use our custom handler
+                        info!("Multi-layer OCI artifact detected, using direct OCI client");
                         
-                        // Create policy file in the same temp directory as the WASM
-                        if let DownloadedResource::Temp((ref tempdir, ref _wasm_path)) = downloaded_resource {
-                            let policy_path = tempdir.path().join(format!("{}.policy.yaml", component_name));
-                            tokio::fs::write(&policy_path, &policy_data)
-                                .await
-                                .context("Failed to save policy file")?;
-                            info!("Policy saved to: {:?}", policy_path);
+                        // Use our new multi-layer support to get ALL layers
+                        let artifact = crate::oci_multi_layer::pull_multi_layer_artifact(&reference, oci_client)
+                            .await
+                            .context("Failed to extract layers from multi-layer OCI artifact")?;
+                        
+                        // Save the WASM data
+                        let component_name = reference.repository().replace('/', "_");
+                        let (downloaded_resource, mut file) = DownloadedResource::new_temp_file(
+                            &component_name,
+                            Self::FILE_EXTENSION,
+                        )
+                        .await?;
+                        
+                        file.write_all(&artifact.wasm_data).await?;
+                        file.flush().await?;
+                        file.sync_all().await?;
+                        drop(file);
+                        
+                        // If there's a policy, save it alongside the WASM in the temp directory
+                        if let Some(policy_data) = artifact.policy_data {
+                            info!("Saving policy layer alongside component");
+                            
+                            // Create policy file in the same temp directory as the WASM
+                            if let DownloadedResource::Temp((ref tempdir, ref _wasm_path)) = downloaded_resource {
+                                let policy_path = tempdir.path().join(format!("{}.policy.yaml", component_name));
+                                tokio::fs::write(&policy_path, &policy_data)
+                                    .await
+                                    .context("Failed to save policy file")?;
+                                info!("Policy saved to: {:?}", policy_path);
+                            }
                         }
+                        
+                        info!("Successfully extracted WASM component and policy from multi-layer artifact");
+                        
+                        Ok(downloaded_resource)
+                    } else {
+                        // Some other oci_wasm error - propagate it
+                        Err(e)
                     }
-                    
-                    info!("Successfully extracted WASM component and policy from multi-layer artifact");
-                    
-                    Ok(downloaded_resource)
                 } else {
                     // Some other error - propagate it
                     Err(e)

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@attackordie attackordie force-pushed the fix/oci-multi-layer-support branch 2 times, most recently from f1890cf to 3090300 Compare August 31, 2025 00:37

@Mossaka Mossaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@attackordie

Copy link
Copy Markdown
Contributor Author

I'll work on getting the checks passing. it appears my dev setup does not match completely.

@Mossaka Mossaka force-pushed the fix/oci-multi-layer-support branch from 3090300 to a19b4ec Compare September 10, 2025 17:00
@Mossaka

Mossaka commented Sep 15, 2025

Copy link
Copy Markdown
Contributor

@attackordie , hey let me know how I can help! I would love to have this PR merged

@attackordie

Copy link
Copy Markdown
Contributor Author

Thanks for the offer. I have some time today for the project and might find you on discord with some questions.

This commit introduces full support for multi-layer OCI images in wassette,
expanding beyond single-layer WASM components. The implementation includes
proper layer extraction, validation, and testing infrastructure while
maintaining backwards compatibility with existing single-layer artifacts.

Modified files:
- crates/wassette/src/oci_multi_layer.rs - Enhanced multi-layer OCI handling
- tests/oci_cncf_spec_test.rs - Updated CNCF specification compliance tests
- tests/oci_integration_test.rs - Enhanced integration test scenarios
- tests/oci_unit_test.rs - Expanded unit test coverage
- Justfile - Added new test commands and GHCR testing support
- Cargo.lock - Updated dependencies for improved OCI registry support

New files:
- Complete multi-layer OCI image processing capabilities
- GHCR backwards compatibility testing with secure token handling
- Extensive test suites covering multi-layer scenarios

The multi-layer support enables more complex component distribution patterns
while preserving the security model and performance characteristics of the
existing single-layer implementation.
@attackordie attackordie force-pushed the fix/oci-multi-layer-support branch from a19b4ec to 32bf6d7 Compare September 16, 2025 20:40
Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>

@Mossaka Mossaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@Mossaka Mossaka merged commit 2a89afe into microsoft:main Sep 17, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wassette OCI loader ignores additional layers, preventing policy attachment from OCI registries

3 participants