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