fix: make mcpb verify/info actually verify PKCS#7 signatures#255
Open
andy-liner wants to merge 1 commit into
Open
fix: make mcpb verify/info actually verify PKCS#7 signatures#255andy-liner wants to merge 1 commit into
mcpb verify/info actually verify PKCS#7 signatures#255andy-liner wants to merge 1 commit into
Conversation
`verifyMcpbFile()` called node-forge's `PkcsSignedData.verify()`, which is not implemented and always throws "PKCS#7 signature verification not yet implemented". The throw was caught and mapped to `status: "unsigned"`, so `mcpb verify` and `mcpb info` reported *every* signed bundle as unsigned, regardless of how it was signed. The `"self-signed"` status was also unreachable: the OS trust-store check returned "unsigned" before it. This implements the detached PKCS#7 verification manually: - the signed `messageDigest` attribute must equal SHA-256 of the content, and - the signer signature must validate over the DER-encoded authenticated attributes (re-tagged as a SET OF). It also reverses the EOCD `comment_length` patch that `signMcpbFile()` applies *after* signing (added in modelcontextprotocol#204): the stored content differs from the signed content by those two bytes, so verification must restore them before hashing or the digest never matches for bundles produced by `mcpb sign`. Trust levels: OS-trusted chain -> "signed"; self-signed (issuer CN == subject CN) -> "self-signed"; valid signature but untrusted, non-self-signed chain -> "unsigned". The self-signed e2e test accepted both "self-signed" and "unsigned", so it never caught this; it now requires "self-signed". Related: modelcontextprotocol#195 (championed verify fix, native crypto; lacks the EOCD reversal needed on current main), modelcontextprotocol#205, modelcontextprotocol#212. Fixes modelcontextprotocol#21. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mcpb verifyandmcpb infocurrently report every signed bundle asunsigned, regardless of how it was signed.Two distinct causes, both fixed here:
node-forge'sPkcsSignedData.verify()is not implemented — it always throws"PKCS#7 signature verification not yet implemented"(forge#1088).verifyMcpbFile()caught the throw and returned{ status: "unsigned" }, so verification never actually ran. The"self-signed"status was also unreachable, because the OS trust-store check returned"unsigned"before it.The EOCD
comment_lengthpatch (fix: update ZIP EOCD comment_length when signing #204) breaks the content digest.signMcpbFile()bumps the ZIP EOCDcomment_lengthby the signature-block length after computing the signature. So the bytes stored on disk differ from the bytes that were signed by exactly those two bytes, and a digest check over the stored content never matches — for any bundle produced bymcpb signon currentmain.Fix
verifyMcpbFile()now verifies the detached PKCS#7 signature manually:messageDigestauthenticated attribute must equalSHA-256(content), andSET OF, as PKCS#7 requires), using the certificate's public key.Before hashing, it reverses the EOCD
comment_lengthpatch (subtracting the signature-block length) so the digest matches whatsignMcpbFile()actually signed.Trust levels:
signedself-signedunsignedunsignedReproduction (before this PR)
unsignrecognizes the signature whileverify/infodo not — because the only failure was the unimplementedp7.verify()throw (and, on currentmain, the EOCD digest mismatch).Tests
The existing self-signed e2e test accepted both
"self-signed"and"unsigned", which is exactly why this regression went unnoticed. It now requires"self-signed"and checks the publisher. Allsign.e2ecases pass (sign/verify self-signed, CA-signed, tamper detection, unsigned, unsign, EOCD comment_length).Relation to existing PRs
cryptoverify): same core idea. However, on currentmainit hashesoriginalContentas-extracted and so still returnsunsignedformcpb sign-produced bundles, because it does not reverse the post-sign EOCDcomment_lengthbump (fix: update ZIP EOCD comment_length when signing #204). This PR includes that reversal, so it works against the currentmcpb signwithout depending on the external-signing flow in feat: add prepare-for-signing and apply-signature for enterprise HSM signing #222.Fixes #21.
🤖 Generated with Claude Code