View on GitHub →
← Back to Home

Secure Your Rust MCP Server

An interactive guide to implementing SchemaPin v1.3.0 in Rust — with offline verification, trust bundles, and pluggable discovery.

The Challenge: Insecure Tool Schemas

The Model Context Protocol (MCP) relies on JSON schemas to define tool capabilities. If a schema is tampered with, an attacker can hijack your AI agent's tools. This is a critical supply chain vulnerability.

SchemaPin solves this by using cryptographic signatures to guarantee that tool schemas are authentic and unmodified. The Rust crate (schemapin) provides:

  • ECDSA P-256 signing and verification
  • Offline verification with trust bundles
  • Pluggable discovery resolvers (HTTP, local files, trust bundles, chained)
  • TOFU key pinning and combined revocation checking

Advanced Demo: Open In Colab

Step 1: Add SchemaPin to Your Project

Add the schemapin crate to your Cargo.toml. By default, no HTTP dependencies are included — everything works offline.

[dependencies]
schemapin = "1.3.0"

To enable HTTP-based discovery (fetches public keys from .well-known endpoints), add the fetch feature:

[dependencies]
schemapin = { version = "1.3.0", features = ["fetch"] }

Step 2: Sign a Schema

Generate a key pair and sign a tool schema using the crate's crypto and canonicalize modules.

use schemapin::crypto::{generate_key_pair, sign_data, calculate_key_id};
use schemapin::canonicalize::canonicalize_and_hash;
use serde_json::json;

// Generate a new ECDSA P-256 key pair
let key_pair = generate_key_pair().unwrap();

// Define the tool schema
let schema = json!({
    "name": "get_issue",
    "description": "Gets the contents of an issue",
    "input_schema": {
        "type": "object",
        "properties": {
            "owner": { "type": "string" },
            "repo": { "type": "string" },
            "issue_number": { "type": "number" }
        },
        "required": ["owner", "repo", "issue_number"]
    }
});

// Canonicalize + hash, then sign
let hash = canonicalize_and_hash(&schema);
let signature = sign_data(&key_pair.private_key_pem, &hash).unwrap();

// Key fingerprint for discovery/pinning
let key_id = calculate_key_id(&key_pair.public_key_pem).unwrap();

println!("Signature: {signature}");
println!("Key ID: {key_id}");

Step 3: Build a Discovery Document

Create a .well-known/schemapin.json discovery document that clients will use to find your public key.

use schemapin::discovery::build_well_known_response;

// Build the discovery response
let discovery = build_well_known_response(
    &key_pair.public_key_pem,
    Some("My Corp"),
    vec![],   // no revoked keys
    "1.3",
);

// Serialize to JSON for hosting at /.well-known/schemapin.json
let json = serde_json::to_string_pretty(&discovery).unwrap();
std::fs::write(".well-known/schemapin.json", &json).unwrap();

Step 4: Verify Offline (No HTTP Required)

The core of SchemaPin v1.3 is offline verification — verify schemas without any network calls using a pre-shared discovery document.

use schemapin::verification::verify_schema_offline;
use schemapin::pinning::KeyPinStore;

// Client-side: verify the schema offline
let mut pin_store = KeyPinStore::new();

let result = verify_schema_offline(
    &schema,
    &signature,
    "example.com",
    "get_issue",
    &discovery,
    None,                // no standalone revocation doc
    &mut pin_store,
);

if result.valid {
    println!("Schema verified for {}", result.domain.unwrap());
    println!("Developer: {}", result.developer_name.unwrap());
    println!("Key pinning: {}", result.key_pinning.unwrap());
    // Safe to use the tool
} else {
    eprintln!("Verification failed: {}", result.error_message.unwrap());
    // Reject the tool
}

Toggle: Valid vs Tampered

See what happens when a schema is modified after signing.

Valid Schema Tampered
Verifying schema for get_issue @ example.com...
Checking key revocation... OK
TOFU key pinning: first_use (pinned)
Canonicalizing and hashing schema...
Verifying ECDSA signature...
Schema verified for example.com
Developer: My Corp
Key pinning: first_use

Step 5: Use Trust Bundles for Air-Gapped Environments

Trust bundles let you pre-share discovery and revocation documents for environments without internet access — CI pipelines, enterprise deployments, or air-gapped networks.

use schemapin::resolver::TrustBundleResolver;
use schemapin::verification::verify_schema_with_resolver;
use schemapin::pinning::KeyPinStore;

// Load a trust bundle from a JSON file
let bundle_json = std::fs::read_to_string("trust-bundle.json").unwrap();
let resolver = TrustBundleResolver::from_json(&bundle_json).unwrap();

// Verify using the resolver
let mut pin_store = KeyPinStore::new();
let result = verify_schema_with_resolver(
    &schema,
    &signature,
    "example.com",
    "get_issue",
    &resolver,
    &mut pin_store,
);

assert!(result.valid);

Trust Bundle Format

A trust bundle packages discovery documents and revocation lists into a single JSON file.

{
  "schemapin_bundle_version": "1.3",
  "created_at": "2026-02-11T00:00:00Z",
  "documents": [
    {
      "domain": "example.com",
      "schema_version": "1.3",
      "developer_name": "My Corp",
      "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
      "revoked_keys": []
    }
  ],
  "revocations": []
}

Step 6: Pluggable Discovery Resolvers

SchemaPin v1.3 provides a SchemaResolver trait with four built-in implementations. Chain them together for flexible fallback strategies.

use schemapin::resolver::{
    ChainResolver, LocalFileResolver,
    TrustBundleResolver, SchemaResolver,
};

// Try trust bundle first, fall back to local files
let bundle_resolver = TrustBundleResolver::from_json(&bundle_json).unwrap();
let file_resolver = LocalFileResolver::new(
    "/etc/schemapin/discovery".into(),
    Some("/etc/schemapin/revocations".into()),
);

let chain = ChainResolver::new(vec![
    Box::new(bundle_resolver),
    Box::new(file_resolver),
]);

// The chain tries each resolver in order until one succeeds
let discovery = chain.resolve_discovery("example.com").unwrap();

Available Resolvers

Resolver Feature Description
TrustBundleResolverdefaultIn-memory trust bundle
LocalFileResolverdefaultReads {domain}.json from a directory
ChainResolverdefaultFirst-wins fallthrough chain
WellKnownResolverfetchHTTP discovery via .well-known