Skip to content

Signing/verification is non-functional: PKCS#7 signature never verified (node-forge stub), fails closed #260

Description

@aosmcleod

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: signverify 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions