mcpb/test/sign.e2e.test.ts
2025-07-08 15:49:07 -07:00

467 lines
10 KiB
TypeScript
Executable file

/**
* End-to-end test for DXT signing and verification
*
* This test:
* 1. Creates a test DXT file
* 2. Generates a self-signed certificate for testing
* 3. Signs the DXT file
* 4. Verifies the signature
* 5. Tests various failure scenarios
*/
import * as fs from "fs";
import forge from "node-forge";
import * as path from "path";
import { signDxtFile, unsignDxtFile, verifyDxtFile } from "../src/node/sign.js";
// Test directory
const TEST_DIR = path.join(__dirname, "test-output");
// Ensure test directory exists
if (!fs.existsSync(TEST_DIR)) {
fs.mkdirSync(TEST_DIR, { recursive: true });
}
// Test file paths
const TEST_DXT = path.join(TEST_DIR, "test.dxt");
const SELF_SIGNED_CERT = path.join(TEST_DIR, "self-signed.crt");
const SELF_SIGNED_KEY = path.join(TEST_DIR, "self-signed.key");
const CA_CERT = path.join(TEST_DIR, "ca.crt");
const CA_KEY = path.join(TEST_DIR, "ca.key");
const SIGNED_CERT = path.join(TEST_DIR, "signed.crt");
const SIGNED_KEY = path.join(TEST_DIR, "signed.key");
/**
* Generate a self-signed certificate for testing
*/
function generateSelfSignedCert() {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: "commonName", value: "Test DXT Publisher" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test Org" },
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// Add extensions
cert.setExtensions([
{
name: "basicConstraints",
cA: false,
},
{
name: "keyUsage",
keyCertSign: false,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: false,
dataEncipherment: false,
},
{
name: "extKeyUsage",
codeSigning: true,
},
]);
// Self-sign
cert.sign(keys.privateKey, forge.md.sha256.create());
// Write files
fs.writeFileSync(SELF_SIGNED_CERT, forge.pki.certificateToPem(cert));
fs.writeFileSync(SELF_SIGNED_KEY, forge.pki.privateKeyToPem(keys.privateKey));
}
/**
* Generate a CA and signed certificate for testing
*/
function generateCASignedCert() {
// Generate CA
const caKeys = forge.pki.rsa.generateKeyPair(2048);
const caCert = forge.pki.createCertificate();
caCert.publicKey = caKeys.publicKey;
caCert.serialNumber = "01";
caCert.validity.notBefore = new Date();
caCert.validity.notAfter = new Date();
caCert.validity.notAfter.setFullYear(
caCert.validity.notBefore.getFullYear() + 10,
);
const caAttrs = [
{ name: "commonName", value: "Test CA" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test CA Org" },
];
caCert.setSubject(caAttrs);
caCert.setIssuer(caAttrs);
caCert.setExtensions([
{
name: "basicConstraints",
cA: true,
},
{
name: "keyUsage",
keyCertSign: true,
digitalSignature: true,
cRLSign: true,
},
]);
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
// Generate signed certificate
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "02";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: "commonName", value: "Test Publisher (CA Signed)" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test Publisher Org" },
];
cert.setSubject(attrs);
cert.setIssuer(caCert.subject.attributes);
cert.setExtensions([
{
name: "basicConstraints",
cA: false,
},
{
name: "keyUsage",
digitalSignature: true,
nonRepudiation: true,
},
{
name: "extKeyUsage",
codeSigning: true,
},
]);
// Sign with CA
cert.sign(caKeys.privateKey, forge.md.sha256.create());
// Write files
fs.writeFileSync(CA_CERT, forge.pki.certificateToPem(caCert));
fs.writeFileSync(CA_KEY, forge.pki.privateKeyToPem(caKeys.privateKey));
fs.writeFileSync(SIGNED_CERT, forge.pki.certificateToPem(cert));
fs.writeFileSync(SIGNED_KEY, forge.pki.privateKeyToPem(keys.privateKey));
}
/**
* Create a test DXT file (just a simple ZIP)
*/
function createTestDxt() {
// Create a simple ZIP file content
// This is a minimal valid ZIP with one file
const zipContent = Buffer.from([
0x50,
0x4b,
0x03,
0x04, // Local file header signature
0x14,
0x00, // Version needed
0x00,
0x00, // Flags
0x00,
0x00, // Compression method (stored)
0x00,
0x00, // Last mod time
0x00,
0x00, // Last mod date
0x00,
0x00,
0x00,
0x00, // CRC-32
0x0d,
0x00,
0x00,
0x00, // Compressed size
0x0d,
0x00,
0x00,
0x00, // Uncompressed size
0x0c,
0x00, // Filename length
0x00,
0x00, // Extra field length
// Filename: "manifest.json"
0x6d,
0x61,
0x6e,
0x69,
0x66,
0x65,
0x73,
0x74,
0x2e,
0x6a,
0x73,
0x6f,
0x6e,
// File content: '{"test":true}'
0x7b,
0x22,
0x74,
0x65,
0x73,
0x74,
0x22,
0x3a,
0x74,
0x72,
0x75,
0x65,
0x7d,
// Central directory
0x50,
0x4b,
0x01,
0x02, // Central directory signature
0x14,
0x00, // Version made by
0x14,
0x00, // Version needed
0x00,
0x00, // Flags
0x00,
0x00, // Compression method
0x00,
0x00, // Last mod time
0x00,
0x00, // Last mod date
0x00,
0x00,
0x00,
0x00, // CRC-32
0x0d,
0x00,
0x00,
0x00, // Compressed size
0x0d,
0x00,
0x00,
0x00, // Uncompressed size
0x0c,
0x00, // Filename length
0x00,
0x00, // Extra field length
0x00,
0x00, // File comment length
0x00,
0x00, // Disk number
0x00,
0x00, // Internal attributes
0x00,
0x00,
0x00,
0x00, // External attributes
0x00,
0x00,
0x00,
0x00, // Offset of local header
// Filename: "manifest.json"
0x6d,
0x61,
0x6e,
0x69,
0x66,
0x65,
0x73,
0x74,
0x2e,
0x6a,
0x73,
0x6f,
0x6e,
// End of central directory
0x50,
0x4b,
0x05,
0x06, // End of central directory signature
0x00,
0x00, // Disk number
0x00,
0x00, // Start disk
0x01,
0x00, // Number of central directory records on this disk
0x01,
0x00, // Total number of central directory records
0x3a,
0x00,
0x00,
0x00, // Size of central directory
0x34,
0x00,
0x00,
0x00, // Offset of central directory
0x00,
0x00, // Comment length
]);
fs.writeFileSync(TEST_DXT, zipContent);
}
/**
* Test signing with self-signed certificate
*/
async function testSelfSignedSigning() {
// Create a copy for this test
const testFile = path.join(TEST_DIR, "test-self-signed.dxt");
fs.copyFileSync(TEST_DXT, testFile);
// Sign the file
expect(() =>
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY),
).not.toThrow();
// Verify the signature
const result = await verifyDxtFile(testFile);
// Self-signed certs may not be trusted by OS, so we accept either status
expect(["self-signed", "unsigned"]).toContain(result.status);
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test signing with CA-signed certificate
*/
async function testCASignedSigning() {
// Create a copy for this test
const testFile = path.join(TEST_DIR, "test-ca-signed.dxt");
fs.copyFileSync(TEST_DXT, testFile);
// Sign the file with intermediate
expect(() =>
signDxtFile(testFile, SIGNED_CERT, SIGNED_KEY, [CA_CERT]),
).not.toThrow();
// Verify the signature
const result = await verifyDxtFile(testFile);
// CA-signed status depends on whether test CA is trusted by OS
expect(["signed", "unsigned"]).toContain(result.status);
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test tampering detection
*/
async function testTamperingDetection() {
// Create a copy and sign it
const testFile = path.join(TEST_DIR, "test-tampered.dxt");
fs.copyFileSync(TEST_DXT, testFile);
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);
// Read the signed file
const signedContent = fs.readFileSync(testFile);
// Tamper with the content (change a byte in the ZIP portion)
const tamperedContent = Buffer.from(signedContent);
tamperedContent[10] = (tamperedContent[10] + 1) % 256;
fs.writeFileSync(testFile, tamperedContent);
// Try to verify - should fail
const result = await verifyDxtFile(testFile);
expect(result.status).toBe("unsigned");
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test unsigned file verification
*/
async function testUnsignedFile() {
const result = await verifyDxtFile(TEST_DXT);
expect(result.status).toBe("unsigned");
}
/**
* Test signature removal
*/
async function testSignatureRemoval() {
// Create a copy and sign it
const testFile = path.join(TEST_DIR, "test-remove-sig.dxt");
fs.copyFileSync(TEST_DXT, testFile);
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);
// Verify it's signed (or at least has signature data)
await verifyDxtFile(testFile);
// Remove signature
expect(() => unsignDxtFile(testFile)).not.toThrow();
// Verify it's unsigned
const afterResult = await verifyDxtFile(testFile);
expect(afterResult.status).toBe("unsigned");
// Clean up
fs.unlinkSync(testFile);
}
describe("DXT Signing E2E Tests", () => {
beforeAll(() => {
// Ensure test directory exists
if (!fs.existsSync(TEST_DIR)) {
fs.mkdirSync(TEST_DIR, { recursive: true });
}
// Setup
generateSelfSignedCert();
generateCASignedCert();
createTestDxt();
});
afterAll(() => {
// Cleanup
try {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
} catch (e) {
// Ignore cleanup errors
}
});
it("should sign and verify with self-signed certificate", async () => {
await testSelfSignedSigning();
});
it("should sign and verify with CA-signed certificate", async () => {
await testCASignedSigning();
});
it("should detect tampering", async () => {
await testTamperingDetection();
});
it("should identify unsigned files", async () => {
await testUnsignedFile();
});
it("should remove signatures", async () => {
await testSignatureRemoval();
});
});