Started work on authenticode support in NodeJS.

Ylian Saint-Hilaire committed May 25, 2022 at 17:37 UTC d0cc25cf5db127ff27decdf334073639ccdd511d
2 files changed +256
MeshCentralServer.njsproj
+1
@@ -99,6 +99,7 @@
99 <Compile Include="amt\amt-wsman.js" />
100 <Compile Include="amt\amt-xml.js" />
101 <Compile Include="amt\amt.js" />
102 + <Compile Include="authenticode.js" />
103 <Compile Include="exeHandler.js" />
104 <Compile Include="amtprovisioningserver.js" />
105 <Compile Include="firebase.js" />
authenticode.js new
+255
@@ -0,0 +1,255 @@
1 +/**
2 +* @description Authenticode parsing
3 +* @author Bryan Roe & Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2022
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +function createAuthenticodeHandler(path) {
10 + const obj = {};
11 + const fs = require('fs');
12 + const crypto = require('crypto');
13 + const forge = require('node-forge');
14 + const pki = forge.pki;
15 + const p7 = forge.pkcs7;
16 + obj.header = { path: path }
17 +
18 + // Read a file slice
19 + function readFileSlice(start, length) {
20 + var buffer = Buffer.alloc(length);
21 + var len = fs.readSync(obj.fd, buffer, 0, buffer.length, start);
22 + if (len < buffer.length) { buffer = buffer.slice(0, len); }
23 + return buffer;
24 + }
25 +
26 + // Close the file
27 + obj.close = function () {
28 + if (obj.fd == null) return;
29 + fs.closeSync(obj.fd);
30 + delete obj.fd;
31 + }
32 +
33 + // Private OIDS
34 + obj.Oids = {
35 + SPC_INDIRECT_DATA_OBJID: '1.3.6.1.4.1.311.2.1.4',
36 + SPC_STATEMENT_TYPE_OBJID: '1.3.6.1.4.1.311.2.1.11',
37 + SPC_SP_OPUS_INFO_OBJID: '1.3.6.1.4.1.311.2.1.12',
38 + SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.21',
39 + SPC_COMMERCIAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.22',
40 + SPC_MS_JAVA_SOMETHING: '1.3.6.1.4.1.311.15.1',
41 + SPC_PE_IMAGE_DATA_OBJID: '1.3.6.1.4.1.311.2.1.15',
42 + SPC_CAB_DATA_OBJID: '1.3.6.1.4.1.311.2.1.25',
43 + SPC_TIME_STAMP_REQUEST_OBJID: '1.3.6.1.4.1.311.3.2.1',
44 + SPC_SIPINFO_OBJID: '1.3.6.1.4.1.311.2.1.30',
45 + SPC_PE_IMAGE_PAGE_HASHES_V1: '1.3.6.1.4.1.311.2.3.1',
46 + SPC_PE_IMAGE_PAGE_HASHES_V2: '1.3.6.1.4.1.311.2.3.2',
47 + SPC_NESTED_SIGNATURE_OBJID: '1.3.6.1.4.1.311.2.4.1',
48 + SPC_RFC3161_OBJID: '1.3.6.1.4.1.311.3.3.1'
49 + }
50 +
51 + // Open the file and read header information
52 + function openFile() {
53 + if (obj.fd != null) return;
54 +
55 + // Open the file descriptor
56 + obj.fd = fs.openSync(path);
57 + obj.stats = fs.fstatSync(obj.fd);
58 + obj.filesize = obj.stats.size;
59 + if (obj.filesize < 64) { throw ('File too short'); }
60 +
61 + // Read the PE header size
62 + var buf = readFileSlice(60, 4);
63 + obj.header.header_size = buf.readUInt32LE(0);
64 +
65 + // Check file size and PE header
66 + if (obj.filesize < (160 + obj.header.header_size)) { throw ('Invalid SizeOfHeaders'); }
67 + if (readFileSlice(obj.header.header_size, 4).toString('hex') != '50450000') { throw ('Invalid PE File'); }
68 +
69 + // Check header magic data
70 + var magic = readFileSlice(obj.header.header_size + 24, 2).readUInt16LE(0);
71 + switch (magic) {
72 + case 0x20b: obj.header.pe32plus = 1; break;
73 + case 0x10b: obj.header.pe32plus = 0; break;
74 + default: throw ('Invalid Magic in PE');
75 + }
76 +
77 + // Read PE header information
78 + obj.header.pe_checksum = readFileSlice(obj.header.header_size + 88, 4).readUInt32LE(0);
79 + obj.header.numRVA = readFileSlice(obj.header.header_size + 116 + (obj.header.pe32plus * 16), 4).readUInt32LE(0);
80 + buf = readFileSlice(obj.header.header_size + 152 + (obj.header.pe32plus * 16), 8);
81 + obj.header.sigpos = buf.readUInt32LE(0);
82 + obj.header.siglen = buf.readUInt32LE(4);
83 + obj.header.signed = ((obj.header.sigpos != 0) && (obj.header.siglen != 0));
84 +
85 + if (obj.header.signed) {
86 + // Read signature block
87 + // TODO: The 3 bytes at the end may be padding we need to remove, not a contant.
88 + var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8 - 3);
89 + var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw));
90 +
91 + // To work around ForgeJS PKCS#7 limitation
92 + // Switch content type from 1.3.6.1.4.1.311.2.1.4 to forge.pki.oids.data (1.2.840.113549.1.7.1)
93 + // TODO: Find forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').data and switch it.
94 + pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
95 +
96 + // Convert the ASN1 content data into binary and place back
97 + var pkcs7content = forge.asn1.toDer(pkcs7der.value[1].value[0].value[2].value[1].value[0]).data;
98 + pkcs7der.value[1].value[0].value[2].value[1].value[0] = { tagClass: 0, type: 4, constructed: false, composed: false, value: pkcs7content };
99 +
100 + // DEBUG: Print out the new DER
101 + //console.log(Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary').toString('hex'));
102 +
103 + // Decode the PKCS7 message
104 + var pkcs7 = p7.messageFromAsn1(pkcs7der);
105 + var pkcs7content = forge.asn1.fromDer(pkcs7.rawCapture.content.value[0].value);
106 +
107 + // Set the certificate chain
108 + obj.certificates = pkcs7.certificates;
109 +
110 + // Get the file hashing algorithm
111 + var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value);
112 + switch (hashAlgoOid) {
113 + case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; }
114 + case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; }
115 + case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; }
116 + case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; }
117 + case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; }
118 + }
119 +
120 + // Get the signed file hash
121 + obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary')
122 +
123 + // Compute the actual file hash
124 + if (obj.fileHashAlgo != null) { obj.fileHashActual = getHash(obj.fileHashAlgo); }
125 + }
126 + }
127 +
128 + // Hash the file using the selected hashing system
129 + function getHash(algo) {
130 + var hash = crypto.createHash(algo);
131 + runHash(hash, 0, obj.header.header_size + 88);
132 + runHash(hash, obj.header.header_size + 88 + 4, obj.header.header_size + 152 + (obj.header.pe32plus * 16));
133 + runHash(hash, obj.header.header_size + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize);
134 + return hash.digest();
135 + }
136 +
137 + // Hash the file from start to end loading 64k chunks
138 + function runHash(hash, start, end) {
139 + var ptr = start;
140 + while (ptr < end) { const buf = readFileSlice(ptr, Math.min(65536, end - ptr)); hash.update(buf); ptr += buf.length; }
141 + }
142 +
143 + // Generate a test self-signed certificate with code signing extension
144 + obj.createSelfSignedCert = function () {
145 + var keys = pki.rsa.generateKeyPair(2048);
146 + var cert = pki.createCertificate();
147 + cert.publicKey = keys.publicKey;
148 + cert.serialNumber = '00000001';
149 + cert.validity.notBefore = new Date();
150 + cert.validity.notAfter = new Date();
151 + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 3);
152 + var attrs = [
153 + { name: 'commonName', value: 'example.org' },
154 + { name: 'countryName', value: 'US' },
155 + { shortName: 'ST', value: 'California' },
156 + { name: 'localityName', value: 'Santa Clara' },
157 + { name: 'organizationName', value: 'Test' },
158 + { shortName: 'OU', value: 'Test' }
159 + ];
160 + cert.setSubject(attrs);
161 + cert.setIssuer(attrs);
162 + cert.setExtensions([{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]);
163 + cert.sign(keys.privateKey, forge.md.sha384.create());
164 + return { cert: cert, key: keys.privateKey };
165 + }
166 +
167 + // Sign the file using the certificate and key. If none is specified, generate a dummy one
168 + obj.sign = function (cert, key) {
169 + if ((cert == null) || (key == null)) { var c = obj.createSelfSignedCert(); cert = c.cert; key = c.key; }
170 + var fileHash = getHash('sha384');
171 + var p7 = forge.pkcs7.createSignedData();
172 + p7.content = forge.util.createBuffer(fileHash, 'utf8');
173 + p7.addCertificate(cert);
174 + p7.addSigner({
175 + key: key,
176 + certificate: cert,
177 + digestAlgorithm: forge.pki.oids.sha384,
178 + authenticatedAttributes:
179 + [
180 + {
181 + type: obj.Oids.SPC_INDIRECT_DATA_OBJID,
182 + },
183 + {
184 + type: forge.pki.oids.contentType,
185 + value: forge.pki.oids.data
186 + },
187 + {
188 + type: forge.pki.oids.messageDigest
189 + // value will be auto-populated at signing time
190 + },
191 + {
192 + type: forge.pki.oids.signingTime,
193 + // value can also be auto-populated at signing time
194 + value: new Date()
195 + }
196 + ]
197 + });
198 + p7.sign();
199 + var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
200 + console.log('p7signature', p7signature.toString('base64'));
201 + }
202 +
203 + openFile();
204 + return obj;
205 +}
206 +
207 +function start() {
208 + // Show tool help
209 + if (process.argv.length < 4) {
210 + console.log("MeshCentral Authenticode Tool.");
211 + console.log("Usage:");
212 + console.log(" node authenticode.js [command] [exepath]");
213 + console.log("Commands:");
214 + console.log(" info - Show information about this executable.");
215 + console.log(" sign - Sign the executable using a dummy certificate.");
216 + return;
217 + }
218 +
219 + // Check that a valid command is passed in
220 + if (['info', 'sign'].indexOf(process.argv[2].toLowerCase()) == -1) {
221 + console.log("Invalid command: " + process.argv[2]);
222 + return;
223 + }
224 +
225 + // Check the file exists
226 + var stats = null;
227 + try { stats = require('fs').statSync(process.argv[3]); } catch (ex) { }
228 + if (stats == null) {
229 + console.log("Unable to open file: " + process.argv[3]);
230 + return;
231 + }
232 +
233 + // Open the file
234 + var exe = createAuthenticodeHandler(process.argv[3]);
235 +
236 + // Execute the command
237 + var command = process.argv[2].toLowerCase();
238 + if (command == 'info') {
239 + console.log('Header', exe.header);
240 + if (exe.fileHashAlgo != null) { console.log('fileHashMethod', exe.fileHashAlgo); }
241 + if (exe.fileHashSigned != null) { console.log('fileHashSigned', exe.fileHashSigned.toString('hex')); }
242 + if (exe.fileHashActual != null) { console.log('fileHashActual', exe.fileHashActual.toString('hex')); }
243 + if (exe.signatureBlock) { console.log('Signature', exe.signatureBlock.toString('hex')); }
244 + }
245 +
246 + if (command == 'sign') {
247 + console.log('Signing...');
248 + exe.sign();
249 + }
250 +
251 + // Close the file
252 + exe.close();
253 +}
254 +
255 +start();
\ No newline at end of file