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:
- 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.
- 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) |
QWED-MCP Audit Finding: LOW-02
Severity: π’ LOW
Component:
provenance.pyβSkillProvenanceGuard._validate_digestAudit ID: LOW-02
Re-audit of v0.2.1: Finding is NEW
Description
_validate_digestverifies only the shape of the digest claim βalgorithm:hexformat, supported algorithm, correct hex length. Nothing in the guard (or anywhere else in the server) computeshash(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
Expected Behavior
Either:
hashlib.new(algo, content).hexdigest()and requires an exact match with the claimed digest. Mismatch β BLOCK. Missing content β UNVERIFIABLE β BLOCK whenrequire_digest=True.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
(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=Truewithout an explicit downgrade marker.)Environment
0.2.1SkillProvenanceGuardsrc/qwed_mcp/security/provenance.py(L201β254)