Skip to content

Commit 8f5f210

Browse files
authored
Merge pull request #204 from modelcontextprotocol/fix/eocd-comment-length
fix: update ZIP EOCD comment_length when signing
2 parents 1dca91e + bb676eb commit 8f5f210

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

src/node/sign.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,20 @@ export function signMcpbFile(
8989
// Create signature block with PKCS#7 data
9090
const signatureBlock = createSignatureBlock(pkcs7Signature);
9191

92+
// Update ZIP EOCD comment_length to include signature block
93+
// This ensures strict ZIP parsers accept the signed file
94+
const updatedContent = Buffer.from(mcpbContent);
95+
const eocdOffset = findEocdOffset(updatedContent);
96+
if (eocdOffset !== -1) {
97+
const currentCommentLength = updatedContent.readUInt16LE(eocdOffset + 20);
98+
updatedContent.writeUInt16LE(
99+
currentCommentLength + signatureBlock.length,
100+
eocdOffset + 20,
101+
);
102+
}
103+
92104
// Append signature block to MCPB file
93-
const signedContent = Buffer.concat([mcpbContent, signatureBlock]);
105+
const signedContent = Buffer.concat([updatedContent, signatureBlock]);
94106
writeFileSync(mcpbPath, signedContent);
95107
}
96108

@@ -218,6 +230,20 @@ export async function verifyMcpbFile(
218230
}
219231
}
220232

233+
/**
234+
* Finds the offset of the ZIP End of Central Directory record
235+
* by scanning backwards for the EOCD magic bytes (0x06054b50)
236+
*/
237+
function findEocdOffset(buffer: Buffer): number {
238+
// EOCD is at least 22 bytes, scan backwards from the end
239+
for (let i = buffer.length - 22; i >= 0; i--) {
240+
if (buffer.readUInt32LE(i) === 0x06054b50) {
241+
return i;
242+
}
243+
}
244+
return -1;
245+
}
246+
221247
/**
222248
* Creates a signature block buffer with PKCS#7 signature
223249
*/

test/sign.e2e.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,50 @@ describe("MCPB Signing E2E Tests", () => {
468468
it("should remove signatures", async () => {
469469
await testSignatureRemoval();
470470
});
471+
472+
it("should update EOCD comment_length after signing", async () => {
473+
const testFile = path.join(TEST_DIR, "test-eocd.mcpb");
474+
fs.copyFileSync(TEST_MCPB, testFile);
475+
476+
// Read original EOCD comment_length
477+
const originalContent = fs.readFileSync(testFile);
478+
let eocdOffset = -1;
479+
for (let i = originalContent.length - 22; i >= 0; i--) {
480+
if (originalContent.readUInt32LE(i) === 0x06054b50) {
481+
eocdOffset = i;
482+
break;
483+
}
484+
}
485+
expect(eocdOffset).toBeGreaterThanOrEqual(0);
486+
const originalCommentLength = originalContent.readUInt16LE(eocdOffset + 20);
487+
expect(originalCommentLength).toBe(0); // Fresh ZIP has no comment
488+
489+
// Sign the file
490+
signMcpbFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);
491+
492+
// Read signed file and verify EOCD comment_length was updated
493+
const signedContent = fs.readFileSync(testFile);
494+
let signedEocdOffset = -1;
495+
for (let i = signedContent.length - 22; i >= 0; i--) {
496+
if (signedContent.readUInt32LE(i) === 0x06054b50) {
497+
signedEocdOffset = i;
498+
break;
499+
}
500+
}
501+
expect(signedEocdOffset).toBeGreaterThanOrEqual(0);
502+
const signedCommentLength = signedContent.readUInt16LE(
503+
signedEocdOffset + 20,
504+
);
505+
506+
// Comment length should equal everything after the EOCD record's original end
507+
const eocdMinSize = 22; // minimum EOCD size (no comment)
508+
const dataAfterEocd =
509+
signedContent.length -
510+
(signedEocdOffset + eocdMinSize + originalCommentLength);
511+
expect(signedCommentLength).toBe(dataAfterEocd);
512+
expect(signedCommentLength).toBeGreaterThan(0);
513+
514+
// Clean up
515+
fs.unlinkSync(testFile);
516+
});
471517
});

0 commit comments

Comments
 (0)