@enconvo/dxt 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ import { execFile } from "child_process";
2
+ import { readFileSync, writeFileSync } from "fs";
3
+ import { mkdtemp, rm, writeFile } from "fs/promises";
4
+ import forge from "node-forge";
5
+ import { tmpdir } from "os";
6
+ import { join } from "path";
7
+ import { promisify } from "util";
8
+ // Signature block markers
9
+ const SIGNATURE_HEADER = "DXT_SIG_V1";
10
+ const SIGNATURE_FOOTER = "DXT_SIG_END";
11
+ const execFileAsync = promisify(execFile);
12
+ /**
13
+ * Signs a DXT file with the given certificate and private key using PKCS#7
14
+ *
15
+ * @param dxtPath Path to the DXT file to sign
16
+ * @param certPath Path to the certificate file (PEM format)
17
+ * @param keyPath Path to the private key file (PEM format)
18
+ * @param intermediates Optional array of intermediate certificate paths
19
+ */
20
+ export function signDxtFile(dxtPath, certPath, keyPath, intermediates) {
21
+ // Read the original DXT file
22
+ const dxtContent = readFileSync(dxtPath);
23
+ // Read certificate and key
24
+ const certificatePem = readFileSync(certPath, "utf-8");
25
+ const privateKeyPem = readFileSync(keyPath, "utf-8");
26
+ // Read intermediate certificates if provided
27
+ const intermediatePems = intermediates?.map((path) => readFileSync(path, "utf-8"));
28
+ // Create PKCS#7 signed data
29
+ const p7 = forge.pkcs7.createSignedData();
30
+ p7.content = forge.util.createBuffer(dxtContent);
31
+ // Parse and add certificates
32
+ const signingCert = forge.pki.certificateFromPem(certificatePem);
33
+ const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
34
+ p7.addCertificate(signingCert);
35
+ // Add intermediate certificates
36
+ if (intermediatePems) {
37
+ for (const pem of intermediatePems) {
38
+ p7.addCertificate(forge.pki.certificateFromPem(pem));
39
+ }
40
+ }
41
+ // Add signer
42
+ p7.addSigner({
43
+ key: privateKey,
44
+ certificate: signingCert,
45
+ digestAlgorithm: forge.pki.oids.sha256,
46
+ authenticatedAttributes: [
47
+ {
48
+ type: forge.pki.oids.contentType,
49
+ value: forge.pki.oids.data,
50
+ },
51
+ {
52
+ type: forge.pki.oids.messageDigest,
53
+ // Value will be auto-populated
54
+ },
55
+ {
56
+ type: forge.pki.oids.signingTime,
57
+ // Value will be auto-populated with current time
58
+ },
59
+ ],
60
+ });
61
+ // Sign with detached signature
62
+ p7.sign({ detached: true });
63
+ // Convert to DER format
64
+ const asn1 = forge.asn1.toDer(p7.toAsn1());
65
+ const pkcs7Signature = Buffer.from(asn1.getBytes(), "binary");
66
+ // Create signature block with PKCS#7 data
67
+ const signatureBlock = createSignatureBlock(pkcs7Signature);
68
+ // Append signature block to DXT file
69
+ const signedContent = Buffer.concat([dxtContent, signatureBlock]);
70
+ writeFileSync(dxtPath, signedContent);
71
+ }
72
+ /**
73
+ * Verifies a signed DXT file using OS certificate store
74
+ *
75
+ * @param dxtPath Path to the signed DXT file
76
+ * @returns Signature information including verification status
77
+ */
78
+ export async function verifyDxtFile(dxtPath) {
79
+ try {
80
+ const fileContent = readFileSync(dxtPath);
81
+ // Find and extract signature block
82
+ const { originalContent, pkcs7Signature } = extractSignatureBlock(fileContent);
83
+ if (!pkcs7Signature) {
84
+ return { status: "unsigned" };
85
+ }
86
+ // Parse PKCS#7 signature
87
+ const asn1 = forge.asn1.fromDer(pkcs7Signature.toString("binary"));
88
+ const p7Message = forge.pkcs7.messageFromAsn1(asn1);
89
+ // Verify it's signed data and cast to correct type
90
+ if (!("type" in p7Message) ||
91
+ p7Message.type !== forge.pki.oids.signedData) {
92
+ return { status: "unsigned" };
93
+ }
94
+ // Now we know it's PkcsSignedData. The types are incorrect, so we'll
95
+ // fix them there
96
+ const p7 = p7Message;
97
+ // Extract certificates from PKCS#7
98
+ const certificates = p7.certificates || [];
99
+ if (certificates.length === 0) {
100
+ return { status: "unsigned" };
101
+ }
102
+ // Get the signing certificate (first one)
103
+ const signingCert = certificates[0];
104
+ // Verify PKCS#7 signature
105
+ const contentBuf = forge.util.createBuffer(originalContent);
106
+ try {
107
+ p7.verify({ authenticatedAttributes: true });
108
+ // Also verify the content matches
109
+ const signerInfos = p7.signerInfos;
110
+ const signerInfo = signerInfos?.[0];
111
+ if (signerInfo) {
112
+ const md = forge.md.sha256.create();
113
+ md.update(contentBuf.getBytes());
114
+ const digest = md.digest().getBytes();
115
+ // Find the message digest attribute
116
+ let messageDigest = null;
117
+ for (const attr of signerInfo.authenticatedAttributes) {
118
+ if (attr.type === forge.pki.oids.messageDigest) {
119
+ messageDigest = attr.value;
120
+ break;
121
+ }
122
+ }
123
+ if (!messageDigest || messageDigest !== digest) {
124
+ return { status: "unsigned" };
125
+ }
126
+ }
127
+ }
128
+ catch (error) {
129
+ return { status: "unsigned" };
130
+ }
131
+ // Convert forge certificate to PEM for OS verification
132
+ const certPem = forge.pki.certificateToPem(signingCert);
133
+ const intermediatePems = certificates
134
+ .slice(1)
135
+ .map((cert) => Buffer.from(forge.pki.certificateToPem(cert)));
136
+ // Verify certificate chain against OS trust store
137
+ const chainValid = await verifyCertificateChain(Buffer.from(certPem), intermediatePems);
138
+ if (!chainValid) {
139
+ // Signature is valid but certificate is not trusted
140
+ return { status: "unsigned" };
141
+ }
142
+ // Extract certificate info
143
+ const isSelfSigned = signingCert.issuer.getField("CN")?.value ===
144
+ signingCert.subject.getField("CN")?.value;
145
+ return {
146
+ status: isSelfSigned ? "self-signed" : "signed",
147
+ publisher: signingCert.subject.getField("CN")?.value || "Unknown",
148
+ issuer: signingCert.issuer.getField("CN")?.value || "Unknown",
149
+ valid_from: signingCert.validity.notBefore.toISOString(),
150
+ valid_to: signingCert.validity.notAfter.toISOString(),
151
+ fingerprint: forge.md.sha256
152
+ .create()
153
+ .update(forge.asn1.toDer(forge.pki.certificateToAsn1(signingCert)).getBytes())
154
+ .digest()
155
+ .toHex(),
156
+ };
157
+ }
158
+ catch (error) {
159
+ throw new Error(`Failed to verify DXT file: ${error}`);
160
+ }
161
+ }
162
+ /**
163
+ * Creates a signature block buffer with PKCS#7 signature
164
+ */
165
+ function createSignatureBlock(pkcs7Signature) {
166
+ const parts = [];
167
+ // Header
168
+ parts.push(Buffer.from(SIGNATURE_HEADER, "utf-8"));
169
+ // PKCS#7 signature length and data
170
+ const sigLengthBuffer = Buffer.alloc(4);
171
+ sigLengthBuffer.writeUInt32LE(pkcs7Signature.length, 0);
172
+ parts.push(sigLengthBuffer);
173
+ parts.push(pkcs7Signature);
174
+ // Footer
175
+ parts.push(Buffer.from(SIGNATURE_FOOTER, "utf-8"));
176
+ return Buffer.concat(parts);
177
+ }
178
+ /**
179
+ * Extracts the signature block from a signed DXT file
180
+ */
181
+ export function extractSignatureBlock(fileContent) {
182
+ // Look for signature footer at the end
183
+ const footerBytes = Buffer.from(SIGNATURE_FOOTER, "utf-8");
184
+ const footerIndex = fileContent.lastIndexOf(footerBytes);
185
+ if (footerIndex === -1) {
186
+ return { originalContent: fileContent };
187
+ }
188
+ // Look for signature header before footer
189
+ const headerBytes = Buffer.from(SIGNATURE_HEADER, "utf-8");
190
+ let headerIndex = -1;
191
+ // Search backwards from footer
192
+ for (let i = footerIndex - 1; i >= 0; i--) {
193
+ if (fileContent.slice(i, i + headerBytes.length).equals(headerBytes)) {
194
+ headerIndex = i;
195
+ break;
196
+ }
197
+ }
198
+ if (headerIndex === -1) {
199
+ return { originalContent: fileContent };
200
+ }
201
+ // Extract original content (everything before signature block)
202
+ const originalContent = fileContent.slice(0, headerIndex);
203
+ // Parse signature block
204
+ let offset = headerIndex + headerBytes.length;
205
+ try {
206
+ // Read PKCS#7 signature length
207
+ const sigLength = fileContent.readUInt32LE(offset);
208
+ offset += 4;
209
+ // Read PKCS#7 signature
210
+ const pkcs7Signature = fileContent.slice(offset, offset + sigLength);
211
+ return {
212
+ originalContent,
213
+ pkcs7Signature,
214
+ };
215
+ }
216
+ catch {
217
+ return { originalContent: fileContent };
218
+ }
219
+ }
220
+ /**
221
+ * Verifies certificate chain against OS trust store
222
+ */
223
+ export async function verifyCertificateChain(certificate, intermediates) {
224
+ let tempDir = null;
225
+ try {
226
+ tempDir = await mkdtemp(join(tmpdir(), "dxt-verify-"));
227
+ const certChainPath = join(tempDir, "chain.pem");
228
+ const certChain = [certificate, ...(intermediates || [])].join("\n");
229
+ await writeFile(certChainPath, certChain);
230
+ // Platform-specific verification
231
+ if (process.platform === "darwin") {
232
+ try {
233
+ await execFileAsync("security", [
234
+ "verify-cert",
235
+ "-c",
236
+ certChainPath,
237
+ "-p",
238
+ "codeSign",
239
+ ]);
240
+ return true;
241
+ }
242
+ catch (error) {
243
+ return false;
244
+ }
245
+ }
246
+ else if (process.platform === "win32") {
247
+ const psCommand = `
248
+ $ErrorActionPreference = 'Stop'
249
+ $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
250
+ $certCollection.Import('${certChainPath}')
251
+
252
+ if ($certCollection.Count -eq 0) {
253
+ Write-Error 'No certificates found'
254
+ exit 1
255
+ }
256
+
257
+ $leafCert = $certCollection[0]
258
+ $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
259
+
260
+ # Enable revocation checking
261
+ $chain.ChainPolicy.RevocationMode = 'Online'
262
+ $chain.ChainPolicy.RevocationFlag = 'EntireChain'
263
+ $chain.ChainPolicy.UrlRetrievalTimeout = New-TimeSpan -Seconds 30
264
+
265
+ # Add code signing application policy
266
+ $codeSignOid = New-Object System.Security.Cryptography.Oid '1.3.6.1.5.5.7.3.3'
267
+ $chain.ChainPolicy.ApplicationPolicy.Add($codeSignOid)
268
+
269
+ # Add intermediate certificates to extra store
270
+ for ($i = 1; $i -lt $certCollection.Count; $i++) {
271
+ [void]$chain.ChainPolicy.ExtraStore.Add($certCollection[$i])
272
+ }
273
+
274
+ # Build and validate chain
275
+ $result = $chain.Build($leafCert)
276
+
277
+ if ($result) {
278
+ 'Valid'
279
+ } else {
280
+ $chain.ChainStatus | ForEach-Object {
281
+ Write-Error "$($_.Status): $($_.StatusInformation)"
282
+ }
283
+ exit 1
284
+ }
285
+ `.trim();
286
+ const { stdout } = await execFileAsync("powershell.exe", [
287
+ "-NoProfile",
288
+ "-NonInteractive",
289
+ "-Command",
290
+ psCommand,
291
+ ]);
292
+ return stdout.includes("Valid");
293
+ }
294
+ else {
295
+ // Linux: Use openssl
296
+ try {
297
+ await execFileAsync("openssl", [
298
+ "verify",
299
+ "-purpose",
300
+ "codesigning",
301
+ "-CApath",
302
+ "/etc/ssl/certs",
303
+ certChainPath,
304
+ ]);
305
+ return true;
306
+ }
307
+ catch (error) {
308
+ return false;
309
+ }
310
+ }
311
+ }
312
+ catch (error) {
313
+ return false;
314
+ }
315
+ finally {
316
+ if (tempDir) {
317
+ try {
318
+ await rm(tempDir, { recursive: true, force: true });
319
+ }
320
+ catch {
321
+ // Ignore cleanup errors
322
+ }
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Removes signature from a DXT file
328
+ */
329
+ export function unsignDxtFile(dxtPath) {
330
+ const fileContent = readFileSync(dxtPath);
331
+ const { originalContent } = extractSignatureBlock(fileContent);
332
+ writeFileSync(dxtPath, originalContent);
333
+ }
@@ -0,0 +1,2 @@
1
+ export declare function validateManifest(inputPath: string): boolean;
2
+ export declare function cleanDxt(inputPath: string): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
+ import * as fs from "fs/promises";
3
+ import { DestroyerOfModules } from "galactus";
4
+ import * as os from "os";
5
+ import { join, resolve } from "path";
6
+ import prettyBytes from "pretty-bytes";
7
+ import { unpackExtension } from "../cli/unpack.js";
8
+ import { DxtManifestSchema } from "../schemas.js";
9
+ import { DxtManifestSchema as LooseDxtManifestSchema } from "../schemas-loose.js";
10
+ export function validateManifest(inputPath) {
11
+ try {
12
+ const resolvedPath = resolve(inputPath);
13
+ let manifestPath = resolvedPath;
14
+ // If input is a directory, look for manifest.json inside it
15
+ if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
16
+ manifestPath = join(resolvedPath, "manifest.json");
17
+ }
18
+ const manifestContent = readFileSync(manifestPath, "utf-8");
19
+ const manifestData = JSON.parse(manifestContent);
20
+ const result = DxtManifestSchema.safeParse(manifestData);
21
+ if (result.success) {
22
+ console.log("Manifest is valid!");
23
+ return true;
24
+ }
25
+ else {
26
+ console.log("ERROR: Manifest validation failed:\n");
27
+ result.error.issues.forEach((issue) => {
28
+ const path = issue.path.join(".");
29
+ console.log(` - ${path ? `${path}: ` : ""}${issue.message}`);
30
+ });
31
+ return false;
32
+ }
33
+ }
34
+ catch (error) {
35
+ if (error instanceof Error) {
36
+ if (error.message.includes("ENOENT")) {
37
+ console.error(`ERROR: File not found: ${inputPath}`);
38
+ if (existsSync(resolve(inputPath)) &&
39
+ statSync(resolve(inputPath)).isDirectory()) {
40
+ console.error(` (No manifest.json found in directory)`);
41
+ }
42
+ }
43
+ else if (error.message.includes("JSON")) {
44
+ console.error(`ERROR: Invalid JSON in manifest file: ${error.message}`);
45
+ }
46
+ else {
47
+ console.error(`ERROR: Error reading manifest: ${error.message}`);
48
+ }
49
+ }
50
+ else {
51
+ console.error("ERROR: Unknown error occurred");
52
+ }
53
+ return false;
54
+ }
55
+ }
56
+ export async function cleanDxt(inputPath) {
57
+ const tmpDir = await fs.mkdtemp(resolve(os.tmpdir(), "dxt-clean-"));
58
+ const dxtPath = resolve(tmpDir, "in.dxt");
59
+ const unpackPath = resolve(tmpDir, "out");
60
+ console.log(" -- Cleaning DXT...");
61
+ try {
62
+ await fs.copyFile(inputPath, dxtPath);
63
+ console.log(" -- Unpacking DXT...");
64
+ await unpackExtension({ dxtPath, silent: true, outputDir: unpackPath });
65
+ const manifestPath = resolve(unpackPath, "manifest.json");
66
+ const originalManifest = await fs.readFile(manifestPath, "utf-8");
67
+ const manifestData = JSON.parse(originalManifest);
68
+ const result = LooseDxtManifestSchema.safeParse(manifestData);
69
+ if (!result.success) {
70
+ throw new Error(`Unrecoverable manifest issues, please run "dxt validate"`);
71
+ }
72
+ await fs.writeFile(manifestPath, JSON.stringify(result.data, null, 2));
73
+ if (originalManifest.trim() !==
74
+ (await fs.readFile(manifestPath, "utf8")).trim()) {
75
+ console.log(" -- Update manifest to be valid per DXT schema");
76
+ }
77
+ else {
78
+ console.log(" -- Manifest already valid per DXT schema");
79
+ }
80
+ const nodeModulesPath = resolve(unpackPath, "node_modules");
81
+ if (existsSync(nodeModulesPath)) {
82
+ console.log(" -- node_modules found, deleting development dependencies");
83
+ const destroyer = new DestroyerOfModules({
84
+ rootDirectory: unpackPath,
85
+ });
86
+ try {
87
+ await destroyer.destroy();
88
+ }
89
+ catch (error) {
90
+ // If modules have already been deleted in a previous clean, the walker
91
+ // will fail when it can't find required dependencies. This is expected
92
+ // and safe to ignore.
93
+ if (error instanceof Error &&
94
+ error.message.includes("Failed to locate module")) {
95
+ console.log(" -- Some modules already removed, skipping remaining cleanup");
96
+ }
97
+ else {
98
+ throw error;
99
+ }
100
+ }
101
+ console.log(" -- Removed development dependencies from node_modules");
102
+ }
103
+ else {
104
+ console.log(" -- No node_modules, not pruning");
105
+ }
106
+ const before = await fs.stat(inputPath);
107
+ const { packExtension } = await import("../cli/pack.js");
108
+ await packExtension({
109
+ extensionPath: unpackPath,
110
+ outputPath: inputPath,
111
+ silent: true,
112
+ });
113
+ const after = await fs.stat(inputPath);
114
+ console.log("\nClean Complete:");
115
+ console.log("Before:", prettyBytes(before.size));
116
+ console.log("After:", prettyBytes(after.size));
117
+ }
118
+ finally {
119
+ await fs.rm(tmpDir, {
120
+ recursive: true,
121
+ force: true,
122
+ });
123
+ }
124
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./node/files.js";
2
+ export * from "./node/sign.js";
3
+ export * from "./node/validate.js";
4
+ export * from "./schemas.js";
5
+ export * from "./shared/config.js";
6
+ export * from "./types.js";
package/dist/node.js ADDED
@@ -0,0 +1,8 @@
1
+ // Node.js-specific exports
2
+ export * from "./node/files.js";
3
+ export * from "./node/sign.js";
4
+ export * from "./node/validate.js";
5
+ // Include all shared exports
6
+ export * from "./schemas.js";
7
+ export * from "./shared/config.js";
8
+ export * from "./types.js";