ADR-011: MCP Tool Signing with Sigstore¶
Status¶
Proposed (January 2026; boundary sync February 2026)
Open-core boundary note (current main): - Open-core delivered: local-key signing and verification via x-assay-sig (assay tool sign --key, assay tool verify). - Enterprise pending: Sigstore keyless + transparency-log verification (Fulcio/Rekor).
Context¶
MCP (Model Context Protocol) tools are vulnerable to supply chain attacks: - 43% of MCP servers are vulnerable to command injection (2025 security research) - Tool definitions can be modified to inject malicious instructions - No built-in verification mechanism in MCP specification
Assay already has ToolIdentity (Phase 9) for hash-based pinning:
// crates/assay-core/src/mcp/identity.rs
pub struct ToolIdentity {
pub server_id: String,
pub tool_name: String,
pub schema_hash: String, // SHA-256 of input schema
pub meta_hash: String, // SHA-256 of description
}
We need to extend this with cryptographic signatures for: 1. Provenance: Who published this tool? 2. Integrity: Has it been tampered with? 3. Non-repudiation: Can we prove authorship?
Decision¶
We will implement Sigstore-based keyless signing as an enterprise extension on top of x-assay-sig.
Signature Format¶
{
"name": "read_file",
"description": "Read contents of a file",
"inputSchema": { ... },
"x-assay-sig": {
"version": 1,
"algorithm": "ecdsa-p256",
"signature": "MEUCIQDx...base64...",
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"rekor_entry": "24296fb24b8ad77a...",
"signed_at": "2026-01-28T12:00:00Z",
"identity": {
"issuer": "https://accounts.google.com",
"subject": "developer@example.com"
}
}
}
Architecture¶
┌─────────────────────────────────────────────────────────────────┐
│ Tool Publisher Workflow │
│ │
│ 1. Developer authenticates via OIDC (GitHub, Google, etc.) │
│ 2. Fulcio issues short-lived certificate binding identity │
│ 3. Tool schema is signed with ephemeral key │
│ 4. Signature + certificate recorded in Rekor transparency log │
│ 5. Tool definition published with x-assay-sig extension │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Tool Consumer Workflow │
│ │
│ 1. Assay loads MCP tool definition │
│ 2. Extract x-assay-sig if present │
│ 3. Verify signature against certificate │
│ 4. Verify certificate chain (Fulcio root) │
│ 5. (Optional) Check Rekor inclusion proof │
│ 6. Compare identity against policy trust anchors │
└─────────────────────────────────────────────────────────────────┘
Signing Flow (CLI)¶
# Keyless signing (enterprise advanced signing)
assay tool sign --keyless tool-definition.json
# Local-key signing (open-core)
assay tool sign --key private.pem tool-definition.json
# Verify a signed tool
assay tool verify tool-definition.json
# Verify with explicit trust requirements
assay tool verify tool-definition.json --require-producer-trust policy.yaml
# Verify and require Rekor transparency proof (enterprise advanced signing)
assay tool verify tool-definition.json --rekor-required
Evidence Verification with Producer Trust¶
Producer trust can also be verified on evidence bundles:
# Verify evidence bundle was produced by trusted identity
assay evidence verify bundle.tar.gz --require-producer-trust
# Explicit trust policy
assay evidence verify bundle.tar.gz --trust-policy org-policy.yaml
Trust policy file format:
# org-policy.yaml
trust_anchors:
- issuer: "https://token.actions.githubusercontent.com"
subject: "repo:myorg/*:ref:refs/heads/main"
- issuer: "https://accounts.google.com"
subject: "*@mycompany.com"
require_transparency: true # enterprise advanced signing
allow_unsigned: false
Verification Logic¶
pub struct SignatureVerifier {
/// Trusted OIDC issuers
trusted_issuers: Vec<String>,
/// Trusted email/subject patterns
trusted_identities: Vec<String>,
/// Fulcio root certificate (from TUF)
fulcio_root: Certificate,
/// Rekor public key (from TUF)
rekor_key: PublicKey,
}
impl SignatureVerifier {
pub fn verify(&self, tool: &ToolDefinition) -> Result<VerifyResult, VerifyError> {
let sig = tool.x_assay_sig.as_ref()
.ok_or(VerifyError::NoSignature)?;
// 1. Verify signature over tool content (JCS canonical form)
let content = canonicalize_tool_jcs(tool)?;
verify_ecdsa(&sig.signature, &content, &sig.certificate)
.map_err(|e| VerifyError::SignatureInvalid { reason: e.to_string() })?;
// 2. Verify certificate chain
verify_certificate_chain(&sig.certificate, &self.fulcio_root)
.map_err(|e| VerifyError::CertificateInvalid { reason: e.to_string() })?;
// 3. Check certificate is not expired
// (Fulcio certs are short-lived, but Rekor proves signing time)
// 4. Verify identity against trust policy
if !self.is_trusted_identity(&sig.identity) {
return Err(VerifyError::ProducerUntrusted {
identity: sig.identity.subject.clone(),
issuer: sig.identity.issuer.clone(),
reason: "Identity not in trust_anchors".into(),
});
}
// 5. (Optional) Verify Rekor inclusion
if let Some(entry_id) = &sig.rekor_entry {
verify_rekor_inclusion(entry_id, &sig.signature)
.map_err(|e| VerifyError::RekorInclusionFailed { reason: e.to_string() })?;
}
Ok(VerifyResult::Verified {
identity: sig.identity.clone(),
signed_at: sig.signed_at,
})
}
}
/// Verification error codes for stable API contracts
#[derive(Debug, Clone)]
pub enum VerifyError {
/// Tool has no x-assay-sig field
NoSignature,
/// Signature does not match content
SignatureInvalid { reason: String },
/// Certificate chain validation failed
CertificateInvalid { reason: String },
/// Certificate has expired (and no Rekor timestamp)
CertificateExpired { expired_at: DateTime<Utc> },
/// Identity not in trust policy
ProducerUntrusted {
identity: String,
issuer: String,
reason: String,
},
/// Rekor inclusion proof failed
RekorInclusionFailed { reason: String },
/// Rekor entry not found
RekorEntryNotFound { entry_id: String },
}
impl VerifyError {
pub fn code(&self) -> &'static str {
match self {
Self::NoSignature => "E_NO_SIGNATURE",
Self::SignatureInvalid { .. } => "E_SIGNATURE_INVALID",
Self::CertificateInvalid { .. } => "E_CERTIFICATE_INVALID",
Self::CertificateExpired { .. } => "E_CERTIFICATE_EXPIRED",
Self::ProducerUntrusted { .. } => "E_PRODUCER_UNTRUSTED",
Self::RekorInclusionFailed { .. } => "E_REKOR_INCLUSION_FAILED",
Self::RekorEntryNotFound { .. } => "E_REKOR_ENTRY_NOT_FOUND",
}
}
}
Policy Integration¶
# assay.yaml
tool_verification:
mode: strict # strict | warn | disabled
trust_anchors:
# Trust specific identities
- issuer: "https://github.com/login/oauth"
subject: "repo:myorg/mcp-tools:ref:refs/heads/main"
# Trust all from an issuer
- issuer: "https://accounts.google.com"
subject: "*@mycompany.com"
# Require Rekor transparency log proof
require_transparency: true
# Allow unsigned tools (for development)
allow_unsigned:
- "localhost:*"
- "test-*"
Content to Sign¶
The signature covers a canonical representation of:
{
"name": "read_file",
"description": "Read contents of a file",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
}
Canonicalization: JCS (RFC 8785) - same as Evidence Contract.
What's NOT signed: - x-assay-sig itself (obviously) - Runtime metadata added by MCP servers - Any fields not in the canonical set
Sigstore Integration¶
Fulcio (Certificate Authority)¶
POST https://fulcio.sigstore.dev/api/v2/signingCert
Authorization: Bearer {oidc_token}
{
"publicKeyRequest": {
"publicKey": {
"algorithm": "ECDSA",
"content": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
},
"proofOfPossession": "MEUCIQD..."
}
}
Rekor (Transparency Log)¶
POST https://rekor.sigstore.dev/api/v1/log/entries
{
"kind": "hashedrekord",
"apiVersion": "0.0.1",
"spec": {
"signature": {
"content": "MEUCIQDx...",
"publicKey": { "content": "MFkw..." }
},
"data": {
"hash": { "algorithm": "sha256", "value": "abc123..." }
}
}
}
SchemaPin Compatibility¶
We align with SchemaPin protocol where possible: - Same canonical JSON format - Compatible signature algorithm (ECDSA P-256) - Similar trust anchor model
This enables interoperability with other MCP security tools.
Alternatives Considered¶
1. Simple Hash Pinning (Current State)¶
Pros: - Already implemented - No external dependencies
Cons: - No provenance (who published?) - Manual hash distribution - No revocation mechanism
Decision: Keep as fallback, extend with signatures.
2. GPG Signatures¶
Pros: - Well-understood - Existing tooling
Cons: - Key management burden - No built-in transparency - Complex trust model
Decision: Too much operational overhead.
3. Custom PKI¶
Pros: - Full control - No external dependencies
Cons: - Must operate CA - No ecosystem adoption - Trust bootstrap problem
Decision: Sigstore provides this as a service.
4. JWT-based Attestation¶
Pros: - Familiar format - OIDC integration
Cons: - Not designed for artifact signing - No transparency log
Decision: Sigstore uses OIDC but produces proper X.509 certificates.
Implementation Plan¶
Phase 1: Enterprise Signing CLI (Week 1)¶
-
assay tool sign --keylesscommand - Fulcio integration for certificate issuance
- Rekor integration for transparency logging
-
x-assay-sigserialization
Phase 2: Verification (Week 2)¶
-
assay tool verifycommand - Policy-based trust anchors
- Integration with existing
ToolIdentity - Warning mode for gradual rollout
Phase 3: MCP Server Integration (Week 3)¶
- Verify tools on MCP server startup
- Runtime policy enforcement
- Unsigned tool handling (warn/block)
Phase 4: Documentation (Week 4)¶
- Publisher guide
- Verifier guide
- Trust policy examples
Acceptance Criteria¶
- Keyless signing produces valid
x-assay-sig - Verification passes for validly signed tools
- Verification fails for tampered tools
- Trust policy correctly filters untrusted identities
- Rekor inclusion proof is verifiable
- CLI UX matches
cosign sign-blob/cosign verify-blob
Consequences¶
Positive¶
- Provenance for enterprise-managed tool definitions
- No key management for enterprise developers using keyless mode
- Transparency via Rekor for the enterprise advanced-signing extension
- Interoperable with Sigstore ecosystem when that extension is enabled
Negative¶
- External dependency (Sigstore infrastructure)
- OIDC login required for signing
- Verification adds latency (~100ms per tool)
Neutral¶
- Must distribute Fulcio/Rekor roots via TUF
- Certificate expiry handled by Rekor timestamp