Skip to content

[Bug]: Provenance digest is format-checked but never content-verified β€” false integrity assuranceΒ #30

Description

QWED-MCP Audit Finding: LOW-02

Severity: 🟒 LOW
Component: provenance.py β€” SkillProvenanceGuard._validate_digest
Audit ID: LOW-02
Re-audit of v0.2.1: Finding is NEW


Description

_validate_digest verifies only the shape of the digest claim β€” algorithm:hex format, supported algorithm, correct hex length. Nothing in the guard (or anywhere else in the server) computes hash(skill_content) and compares it against the claimed digest.

A poisoned skill can therefore carry any well-formed digest:

{
  "name": "helpful-tool",
  "version": "1.0.0",
  "registry": "github.com",
  "source_url": "https://github.com/org/repo",
  "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "download_count": 5000
}

The digest above is the SHA-256 of the empty string β€” it passes all format checks while attesting to nothing about the skill's actual content. The guard currently answers "is this string digest-shaped?" when operators believe it answers "does this skill match its attested digest?".


Why This Violates QWED Philosophy

  • Principle 8 β€” Verify Claims, Not Sources: The digest is a claim about content. The guard validates the claim's syntax but never verifies the claim itself. As implemented, the digest requirement provides false assurance β€” arguably worse than no digest field, because it looks like integrity verification in audit output.

Expected Behavior

Either:

  1. Wire content verification: the manifest (or the skill loader) must provide the skill artifact bytes; the guard computes hashlib.new(algo, content).hexdigest() and requires an exact match with the claimed digest. Mismatch β†’ BLOCK. Missing content β†’ UNVERIFIABLE β†’ BLOCK when require_digest=True.
  2. Or downgrade the claim: if content is architecturally unavailable at this layer, rename/report the check explicitly as digest_format_validated (not verified) so audit output cannot be misread as integrity proof.

Option 1 is the philosophically aligned fix; Option 2 is the minimum honest state.


Suggested Fix Direction

def _validate_digest(self, manifest: Dict[str, Any], content: Optional[bytes]) -> List[str]:
    ...
    if content is not None:
        actual = hashlib.new(algo.lower(), content).hexdigest()
        if not hmac.compare_digest(actual, digest.lower()):
            findings.append("Digest mismatch β€” skill content does not match attested digest")
    else:
        findings.append("Digest claim unverifiable β€” skill content not provided")

(Where the content comes from β€” manifest field, loader callback, or artifact path β€” is an architecture decision; the invariant is that format-validated-but-content-unverified digests must not produce verified=True without an explicit downgrade marker.)


Environment

Field Value
QWED-MCP Version 0.2.1
Component SkillProvenanceGuard
File src/qwed_mcp/security/provenance.py (L201–254)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions