Summary
The bundle signing / verification subsystem in src/node/sign.ts does not actually perform cryptographic signature verification. It currently fails closed (files signed by the tool report as unsigned), so this is a functionality break today — but the structure is also a latent security problem because the trust decision is made by a swallowed exception rather than by a verified signature.
Reproduced end-to-end against the compiled CLI: sign a file with mcpb sign, then mcpb verify reports "Extension is not signed."
Findings
1. PKCS#7 signature is never verified (critical) — src/node/sign.ts:164-190
The pinned dependency node-forge@^1.3.2 does not implement PKCS#7 verification: forge.pkcs7.PkcsSignedData.verify() unconditionally throw new Error('PKCS#7 signature verification not yet implemented.'). verifyMcpbFile calls p7.verify({ authenticatedAttributes: true }) inside a try, and the catch returns { status: "unsigned" }. So no signed file is ever cryptographically validated — the result is decided by the thrown exception, not the signature.
2. Content-digest (messageDigest) check is dead code on detached signatures (high) — src/node/sign.ts:167-187
For a detached signature forge.pkcs7.messageFromAsn1(...) returns a message whose signerInfos is empty, so signerInfo = signerInfos?.[0] is undefined and the entire digest-comparison block is skipped. Even with a non-detached message, forge's _signerFromAsn1 leaves authenticatedAttributes empty, so messageDigest is never found. Nothing in code binds the signature to the archive bytes.
3. Signed byte-region differs from verified byte-region (high) — src/node/sign.ts:46 vs 94-105 / 300
signMcpbFile signs the pristine archive (EOCD comment_length = 0), then writes a copy with comment_length bumped by the signature-block length. On verify, extractSignatureBlock returns the bumped bytes. So even a working verifier would hash different bytes than were signed and reject every file the tool produces. The mutated 2-byte EOCD field also sits outside the signed region.
Reproduction
mcpb sign test.mcpb --cert cert.pem --key key.pem # "Successfully signed"
mcpb verify test.mcpb # ERROR: Extension is not signed
Directly: p7.verify(...) throws PKCS#7 signature verification not yet implemented.; re-parsing the signed file gives signerInfos.length === 0; signed EOCD comment_length 0 vs verified 1225.
Suggested fix
This is a design change, not a one-liner:
- Do not rely on
node-forge for PKCS#7/CMS verification — it does not implement it. Use a verifier that actually verifies (e.g. shell out to openssl cms -verify with the detached signature + content, or a maintained CMS library).
- Sign exactly the bytes that will be extracted and hashed at verify time (apply any EOCD mutation before signing, or define and extract an unambiguous signed region).
- Restructure so that any error or non-
true verification result is a hard reject. Distinguish "no signature present" (genuinely unsigned) from "signature present but failed/errored" (must reject).
- Add a round-trip test:
sign → verify must report signed; tampering with any byte must fail verification.
Notes
Lower-severity observations in the same file (not blocking): self-signed detection compares only issuer CN vs subject CN (sign.ts:210-212); certificate validity window isn't asserted in-code and depends on per-OS tools (sign.ts:325).
Environment: reproduced on the current main (70fe3b3) with node-forge 1.3.3.
Summary
The bundle signing / verification subsystem in
src/node/sign.tsdoes not actually perform cryptographic signature verification. It currently fails closed (files signed by the tool report as unsigned), so this is a functionality break today — but the structure is also a latent security problem because the trust decision is made by a swallowed exception rather than by a verified signature.Reproduced end-to-end against the compiled CLI: sign a file with
mcpb sign, thenmcpb verifyreports "Extension is not signed."Findings
1. PKCS#7 signature is never verified (critical) —
src/node/sign.ts:164-190The pinned dependency
node-forge@^1.3.2does not implement PKCS#7 verification:forge.pkcs7.PkcsSignedData.verify()unconditionallythrow new Error('PKCS#7 signature verification not yet implemented.').verifyMcpbFilecallsp7.verify({ authenticatedAttributes: true })inside atry, and thecatchreturns{ status: "unsigned" }. So no signed file is ever cryptographically validated — the result is decided by the thrown exception, not the signature.2. Content-digest (messageDigest) check is dead code on detached signatures (high) —
src/node/sign.ts:167-187For a detached signature
forge.pkcs7.messageFromAsn1(...)returns a message whosesignerInfosis empty, sosignerInfo = signerInfos?.[0]isundefinedand the entire digest-comparison block is skipped. Even with a non-detached message, forge's_signerFromAsn1leavesauthenticatedAttributesempty, somessageDigestis never found. Nothing in code binds the signature to the archive bytes.3. Signed byte-region differs from verified byte-region (high) —
src/node/sign.ts:46vs94-105/300signMcpbFilesigns the pristine archive (EOCDcomment_length = 0), then writes a copy withcomment_lengthbumped by the signature-block length. On verify,extractSignatureBlockreturns the bumped bytes. So even a working verifier would hash different bytes than were signed and reject every file the tool produces. The mutated 2-byte EOCD field also sits outside the signed region.Reproduction
Directly:
p7.verify(...)throwsPKCS#7 signature verification not yet implemented.; re-parsing the signed file givessignerInfos.length === 0; signed EOCDcomment_length0 vs verified 1225.Suggested fix
This is a design change, not a one-liner:
node-forgefor PKCS#7/CMS verification — it does not implement it. Use a verifier that actually verifies (e.g. shell out toopenssl cms -verifywith the detached signature + content, or a maintained CMS library).trueverification result is a hard reject. Distinguish "no signature present" (genuinely unsigned) from "signature present but failed/errored" (must reject).sign→verifymust report signed; tampering with any byte must fail verification.Notes
Lower-severity observations in the same file (not blocking): self-signed detection compares only issuer CN vs subject CN (
sign.ts:210-212); certificate validity window isn't asserted in-code and depends on per-OS tools (sign.ts:325).Environment: reproduced on the current
main(70fe3b3) withnode-forge1.3.3.