Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/node/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,20 @@ export function signMcpbFile(
// Create signature block with PKCS#7 data
const signatureBlock = createSignatureBlock(pkcs7Signature);

// Update ZIP EOCD comment_length to include signature block
// This ensures strict ZIP parsers accept the signed file
const updatedContent = Buffer.from(mcpbContent);
const eocdOffset = findEocdOffset(updatedContent);
if (eocdOffset !== -1) {
const currentCommentLength = updatedContent.readUInt16LE(eocdOffset + 20);
updatedContent.writeUInt16LE(
currentCommentLength + signatureBlock.length,
eocdOffset + 20,
);
}

// Append signature block to MCPB file
const signedContent = Buffer.concat([mcpbContent, signatureBlock]);
const signedContent = Buffer.concat([updatedContent, signatureBlock]);
writeFileSync(mcpbPath, signedContent);
}

Expand Down Expand Up @@ -218,6 +230,20 @@ export async function verifyMcpbFile(
}
}

/**
* Finds the offset of the ZIP End of Central Directory record
* by scanning backwards for the EOCD magic bytes (0x06054b50)
*/
function findEocdOffset(buffer: Buffer): number {
// EOCD is at least 22 bytes, scan backwards from the end
for (let i = buffer.length - 22; i >= 0; i--) {
if (buffer.readUInt32LE(i) === 0x06054b50) {
return i;
}
}
return -1;
}

/**
* Creates a signature block buffer with PKCS#7 signature
*/
Expand Down
46 changes: 46 additions & 0 deletions test/sign.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,50 @@ describe("MCPB Signing E2E Tests", () => {
it("should remove signatures", async () => {
await testSignatureRemoval();
});

it("should update EOCD comment_length after signing", async () => {
const testFile = path.join(TEST_DIR, "test-eocd.mcpb");
fs.copyFileSync(TEST_MCPB, testFile);

// Read original EOCD comment_length
const originalContent = fs.readFileSync(testFile);
let eocdOffset = -1;
for (let i = originalContent.length - 22; i >= 0; i--) {
if (originalContent.readUInt32LE(i) === 0x06054b50) {
eocdOffset = i;
break;
}
}
expect(eocdOffset).toBeGreaterThanOrEqual(0);
const originalCommentLength = originalContent.readUInt16LE(eocdOffset + 20);
expect(originalCommentLength).toBe(0); // Fresh ZIP has no comment

// Sign the file
signMcpbFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);

// Read signed file and verify EOCD comment_length was updated
const signedContent = fs.readFileSync(testFile);
let signedEocdOffset = -1;
for (let i = signedContent.length - 22; i >= 0; i--) {
if (signedContent.readUInt32LE(i) === 0x06054b50) {
signedEocdOffset = i;
break;
}
}
expect(signedEocdOffset).toBeGreaterThanOrEqual(0);
const signedCommentLength = signedContent.readUInt16LE(
signedEocdOffset + 20,
);

// Comment length should equal everything after the EOCD record's original end
const eocdMinSize = 22; // minimum EOCD size (no comment)
const dataAfterEocd =
signedContent.length -
(signedEocdOffset + eocdMinSize + originalCommentLength);
expect(signedCommentLength).toBe(dataAfterEocd);
expect(signedCommentLength).toBeGreaterThan(0);

// Clean up
fs.unlinkSync(testFile);
});
});
Loading