Added time stamping feature to authenticode.js

Ylian Saint-Hilaire committed Jun 17, 2022 at 20:56 UTC e8a67c4c704eb8d515ec30f400f3a4ae3964daeb
1 file changed +342 -70
authenticode.js
+342 -70
@@ -288,12 +288,10 @@ function createAuthenticodeHandler(path) {
288 var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
289 if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
290
291 - //console.log('pkcs7raw', Buffer.from(pkcs7raw, 'binary').toString('base64'));
292 -
291 // Decode the signature block
292 var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw));
293
296 - // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForjeJS adds support for it in the future
294 + // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
295 // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
296 pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
297
@@ -330,7 +328,7 @@ function createAuthenticodeHandler(path) {
328 ) {
329 var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value;
330 if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else {
333 - var r = ""; // This string value is in UCS2 format, convert it to a normal string.
331 + var r = ''; // This string value is in UCS2 format, convert it to a normal string.
332 for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); }
333 obj.signingAttribs.push(r);
334 }
@@ -366,7 +364,7 @@ function createAuthenticodeHandler(path) {
364 }
365
366 // Make a timestamp signature request
369 - obj.timeStampRequest = function (url, func) {
367 + obj.timeStampRequest = function (args, func) {
368 // Create the timestamp request in DER format
369 const asn1 = forge.asn1;
370 const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
@@ -398,20 +396,90 @@ function createAuthenticodeHandler(path) {
396 }
397 };
398
401 - console.log('options', options);
402 - console.log('requestBody', requestBody);
403 -
404 - // Debug
405 - const sampleResponse = "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";
406 - func(null, sampleResponse);
407 - return;
408 -
399 // Set up the request
400 var responseAccumulator = '';
411 - var req = http.request(url, options, function (res) {
401 + var req = http.request(args.time, options, function (res) {
402 res.setEncoding('utf8');
403 res.on('data', function (chunk) { responseAccumulator += chunk; });
414 - res.on('end', function () { func(null, Buffer.from(responseAccumulator, 'base64').toString('base64')); });
404 + res.on('end', function () {
405 + // Decode the timestamp signature block
406 + const timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(responseAccumulator, 'base64').toString('binary')));
407 +
408 + // Decode the executable signature block
409 + const pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary')));
410 +
411 + // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
412 + // TODO: We could look to see if the certificate is already present in the executable
413 + const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
414 + for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
415 +
416 + // Get the time signature and add it to the executables PKCS7
417 + const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
418 + const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
419 + const asn1obj2 =
420 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
421 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
422 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
423 + timeasn1Signature
424 + ])
425 + ]);
426 + pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
427 +
428 + // Re-encode the executable signature block
429 + const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
430 +
431 + // Open the output file
432 + var output = null;
433 + try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
434 + if (output == null) return false;
435 + var tmp, written = 0;
436 + var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
437 +
438 + // Compute pre-header length and copy that to the new file
439 + var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
440 + var tmp = readFileSlice(written, preHeaderLen);
441 + fs.writeSync(output, tmp);
442 + written += tmp.length;
443 +
444 + // Quad Align the results, adding padding if necessary
445 + var len = executableSize + p7signature.length;
446 + var padding = (8 - ((len) % 8)) % 8;
447 +
448 + // Write the signature header
449 + var addresstable = Buffer.alloc(8);
450 + addresstable.writeUInt32LE(executableSize);
451 + addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
452 + fs.writeSync(output, addresstable);
453 + written += addresstable.length;
454 +
455 + // Copy the rest of the file until the start of the signature block
456 + while ((executableSize - written) > 0) {
457 + tmp = readFileSlice(written, Math.min(executableSize - written, 65536));
458 + fs.writeSync(output, tmp);
459 + written += tmp.length;
460 + }
461 +
462 + // Write the signature block header and signature
463 + var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
464 + win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
465 + win.writeUInt16LE(512, 4); // WORD revision
466 + win.writeUInt16LE(2, 6); // WORD type
467 + fs.writeSync(output, win);
468 + fs.writeSync(output, p7signature);
469 + if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
470 + written += (p7signature.length + padding + 8);
471 +
472 + // Compute the checksum and write it in the PE header checksum location
473 + var tmp = Buffer.alloc(4);
474 + tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
475 + fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
476 +
477 + // Close the file
478 + fs.closeSync(output);
479 +
480 + // Indicate we are done
481 + func(null);
482 + });
483 });
484
485 // Post the data
@@ -1110,7 +1178,7 @@ function createAuthenticodeHandler(path) {
1178 }
1179
1180 // Sign the file using the certificate and key. If none is specified, generate a dummy one
1113 - obj.sign = function (cert, args) {
1181 + obj.sign = function (cert, args, func) {
1182 if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
1183
1184 // Set the hash algorithm hash OID
@@ -1121,16 +1189,16 @@ function createAuthenticodeHandler(path) {
1189 if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
1190 if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
1191 if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
1124 - if (hashOid == null) return false;
1192 + if (hashOid == null) { func(false); return; };
1193
1194 // Create the signature block
1127 - var p7 = forge.pkcs7.createSignedData();
1195 + var xp7 = forge.pkcs7.createSignedData();
1196 var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
1129 - p7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1130 - p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1131 - p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1132 - p7.addCertificate(cert.cert);
1133 - if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { p7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1197 + xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1198 + xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1199 + xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1200 + xp7.addCertificate(cert.cert);
1201 + if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1202
1203 // Build authenticated attributes
1204 var authenticatedAttributes = [
@@ -1149,22 +1217,109 @@ function createAuthenticodeHandler(path) {
1217 }
1218
1219 // Add the signer and sign
1152 - p7.addSigner({
1220 + xp7.addSigner({
1221 key: cert.key,
1222 certificate: cert.cert,
1223 digestAlgorithm: forge.pki.oids.sha384,
1224 authenticatedAttributes: authenticatedAttributes
1225 });
1158 - p7.sign();
1159 - var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1160 - //console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
1226 + xp7.sign();
1227 + var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1228 +
1229 + if (args.time == null) {
1230 + // Sign the executable without timestamp
1231 + signEx(args, p7signature, obj.filesize, func);
1232 + } else {
1233 + // Decode the signature block
1234 + var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature));
1235 +
1236 + // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
1237 + // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
1238 + pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
1239 +
1240 + // Decode the PKCS7 message
1241 + var pkcs7 = p7.messageFromAsn1(pkcs7der);
1242
1243 + // Create the timestamp request in DER format
1244 + const asn1 = forge.asn1;
1245 + const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
1246 + const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
1247 + const asn1obj =
1248 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1249 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
1250 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1251 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
1252 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
1253 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
1254 + ])
1255 + ])
1256 + ]);
1257 +
1258 + // Re-decode the PKCS7 from the executable, this time, no workaround needed
1259 + pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature));
1260 +
1261 + // Serialize an ASN.1 object to DER format in Base64
1262 + const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
1263 +
1264 + // Make an HTTP request
1265 + const http = require('http');
1266 + var options = {
1267 + method: 'POST',
1268 + headers: {
1269 + 'accept': 'application/octet-stream',
1270 + 'cache-control': 'no-cache',
1271 + 'user-agent': 'Transport',
1272 + 'content-type': 'application/octet-stream',
1273 + 'content-length': Buffer.byteLength(requestBody)
1274 + }
1275 + };
1276 +
1277 + // Set up the request
1278 + var responseAccumulator = '';
1279 + var req = http.request(args.time, options, function (res) {
1280 + res.setEncoding('utf8');
1281 + res.on('data', function (chunk) { responseAccumulator += chunk; });
1282 + res.on('end', function () {
1283 + // Decode the timestamp signature block
1284 + const timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(responseAccumulator, 'base64').toString('binary')));
1285 +
1286 + // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
1287 + // TODO: We could look to see if the certificate is already present in the executable
1288 + const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1289 + for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1290 +
1291 + // Get the time signature and add it to the executables PKCS7
1292 + const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1293 + const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1294 + const asn1obj2 =
1295 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1296 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1297 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
1298 + timeasn1Signature
1299 + ])
1300 + ]);
1301 + pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
1302 +
1303 + // Re-encode the executable signature block
1304 + const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
1305 +
1306 + // Write the file with the signature block
1307 + signEx(args, p7signature, obj.filesize, func);
1308 + });
1309 + });
1310 +
1311 + // Post the data
1312 + req.write(requestBody);
1313 + req.end();
1314 + }
1315 + }
1316 +
1317 + function signEx(args, p7signature, filesize, func) {
1318 // Open the output file
1319 var output = null;
1320 try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
1165 - if (output == null) return false;
1166 - var tmp, written = 0;
1167 - var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
1321 + if (output == null) { func(false); return; }
1322 + var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
1323
1324 // Compute pre-header length and copy that to the new file
1325 var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
@@ -1207,7 +1362,7 @@ function createAuthenticodeHandler(path) {
1362
1363 // Close the file
1364 fs.closeSync(output);
1210 - return true;
1365 + func(null);
1366 }
1367
1368 // Save an executable without the signature
@@ -1242,7 +1397,7 @@ function createAuthenticodeHandler(path) {
1397 }
1398
1399 // Save the executable
1245 - obj.writeExecutable = function (args, cert) {
1400 + obj.writeExecutable = function (args, cert, func) {
1401 // Open the file
1402 var output = fs.openSync(args.out, 'w+');
1403 var tmp, written = 0;
@@ -1340,13 +1495,13 @@ function createAuthenticodeHandler(path) {
1495 if (hashOid == null) return false;
1496
1497 // Create the signature block
1343 - var p7 = forge.pkcs7.createSignedData();
1498 + var xp7 = forge.pkcs7.createSignedData();
1499 var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
1345 - p7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1346 - p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1347 - p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1348 - p7.addCertificate(cert.cert);
1349 - if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { p7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1500 + xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1501 + xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1502 + xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1503 + xp7.addCertificate(cert.cert);
1504 + if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1505
1506 // Build authenticated attributes
1507 var authenticatedAttributes = [
@@ -1365,45 +1520,144 @@ function createAuthenticodeHandler(path) {
1520 }
1521
1522 // Add the signer and sign
1368 - p7.addSigner({
1523 + xp7.addSigner({
1524 key: cert.key,
1525 certificate: cert.cert,
1526 digestAlgorithm: forge.pki.oids.sha384,
1527 authenticatedAttributes: authenticatedAttributes
1528 });
1374 - p7.sign();
1375 - var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1529 + xp7.sign();
1530 + var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1531 //console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
1532
1378 - // Quad Align the results, adding padding if necessary
1379 - var len = written + p7signature.length;
1380 - var padding = (8 - ((len) % 8)) % 8;
1381 -
1382 - // Write the signature block header and signature
1383 - var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
1384 - win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
1385 - win.writeUInt16LE(512, 4); // WORD revision
1386 - win.writeUInt16LE(2, 6); // WORD type
1387 - fs.writeSync(output, win);
1388 - fs.writeSync(output, p7signature);
1389 - if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
1390 -
1391 - // Write the signature header
1392 - var addresstable = Buffer.alloc(8);
1393 - addresstable.writeUInt32LE(written);
1394 - addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
1395 - var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1396 - fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
1397 - written += (p7signature.length + padding + 8); // Add the signature block to written counter
1398 -
1399 - // Compute the checksum and write it in the PE header checksum location
1400 - var tmp = Buffer.alloc(4);
1401 - tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
1402 - fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
1533 + if (args.time == null) {
1534 + // Write the signature block to the output executable without time stamp
1535 + writeExecutableEx(output, p7signature, written, func);
1536 + } else {
1537 + // Decode the signature block
1538 + var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature));
1539 +
1540 + // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
1541 + // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
1542 + pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
1543 +
1544 + // Decode the PKCS7 message
1545 + var pkcs7 = p7.messageFromAsn1(pkcs7der);
1546 +
1547 + // Create the timestamp request in DER format
1548 + const asn1 = forge.asn1;
1549 + const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
1550 + const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
1551 + const asn1obj =
1552 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1553 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
1554 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1555 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
1556 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
1557 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
1558 + ])
1559 + ])
1560 + ]);
1561 +
1562 + // Re-decode the PKCS7 from the executable, this time, no workaround needed
1563 + pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature));
1564 +
1565 + // Serialize an ASN.1 object to DER format in Base64
1566 + const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
1567 +
1568 + // Make an HTTP request
1569 + const http = require('http');
1570 + var options = {
1571 + method: 'POST',
1572 + headers: {
1573 + 'accept': 'application/octet-stream',
1574 + 'cache-control': 'no-cache',
1575 + 'user-agent': 'Transport',
1576 + 'content-type': 'application/octet-stream',
1577 + 'content-length': Buffer.byteLength(requestBody)
1578 + }
1579 + };
1580 +
1581 + // Set up the request
1582 + var responseAccumulator = '';
1583 + var req = http.request(args.time, options, function (res) {
1584 + res.setEncoding('utf8');
1585 + res.on('data', function (chunk) { responseAccumulator += chunk; });
1586 + res.on('end', function () {
1587 + // Decode the timestamp signature block
1588 + const timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(responseAccumulator, 'base64').toString('binary')));
1589 +
1590 + // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
1591 + // TODO: We could look to see if the certificate is already present in the executable
1592 + const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1593 + for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1594 +
1595 + // Get the time signature and add it to the executables PKCS7
1596 + const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1597 + const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1598 + const asn1obj2 =
1599 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1600 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1601 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
1602 + timeasn1Signature
1603 + ])
1604 + ]);
1605 + pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
1606 +
1607 + // Re-encode the executable signature block
1608 + const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
1609 +
1610 + // Write the file with the signature block
1611 + writeExecutableEx(output, p7signature, written, func);
1612 + });
1613 + });
1614 +
1615 + // Post the data
1616 + req.write(requestBody);
1617 + req.end();
1618 + }
1619 + return;
1620 }
1621
1622 // Close the file
1623 fs.closeSync(output);
1624 +
1625 + // Indicate success
1626 + func(null);
1627 + }
1628 +
1629 + function writeExecutableEx(output, p7signature, written, func) {
1630 + // Quad Align the results, adding padding if necessary
1631 + var len = written + p7signature.length;
1632 + var padding = (8 - ((len) % 8)) % 8;
1633 +
1634 + // Write the signature block header and signature
1635 + var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
1636 + win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
1637 + win.writeUInt16LE(512, 4); // WORD revision
1638 + win.writeUInt16LE(2, 6); // WORD type
1639 + fs.writeSync(output, win);
1640 + fs.writeSync(output, p7signature);
1641 + if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
1642 +
1643 + // Write the signature header
1644 + var addresstable = Buffer.alloc(8);
1645 + addresstable.writeUInt32LE(written);
1646 + addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
1647 + var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1648 + fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
1649 + written += (p7signature.length + padding + 8); // Add the signature block to written counter
1650 +
1651 + // Compute the checksum and write it in the PE header checksum location
1652 + var tmp = Buffer.alloc(4);
1653 + tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
1654 + fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
1655 +
1656 + // Close the file
1657 + fs.closeSync(output);
1658 +
1659 + // Indicate success
1660 + func(null);
1661 }
1662
1663 // Return null if we could not open the file
@@ -1430,6 +1684,7 @@ function start() {
1684 console.log(" --desc [description] Description string to embbed into signature.");
1685 console.log(" --url [url] URL to embbed into signature.");
1686 console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
1687 + console.log(" --time [url] The time signing server URL.");
1688 console.log(" unsign: Remove the signature from the executable.");
1689 console.log(" --exe [file] Required executable to un-sign.");
1690 console.log(" --out [file] Resulting executable with signature removed.");
@@ -1442,6 +1697,10 @@ function start() {
1697 console.log(" --org [value] Certificate organization name.");
1698 console.log(" --ou [value] Certificate organization unit name.");
1699 console.log(" --serial [value] Certificate serial number.");
1700 + console.log(" timestamp: Add a signed timestamp to an already signed executable.");
1701 + console.log(" --exe [file] Required executable to sign.");
1702 + console.log(" --out [file] Resulting signed executable.");
1703 + console.log(" --time [url] The time signing server URL.");
1704 console.log("");
1705 console.log("Note that certificate PEM files must first have the signing certificate,");
1706 console.log("followed by all certificates that form the trust chain.");
@@ -1532,10 +1791,18 @@ function start() {
1791 if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); }
1792 if (resChanges == false) {
1793 console.log("Signing to " + args.out);
1535 - exe.sign(cert, args); // Simple signing, copy most of the original file.
1794 + exe.sign(cert, args, function (err) { // Simple signing, copy most of the original file.
1795 + if (err == null) { console.log("Done."); } else { console.log(err); }
1796 + if (exe != null) { exe.close(); }
1797 + });
1798 + return;
1799 } else {
1800 console.log("Changing resources and signing to " + args.out);
1538 - exe.writeExecutable(args, cert); // Signing with resources decoded and re-encoded.
1801 + exe.writeExecutable(args, cert, function (err) { // Signing with resources decoded and re-encoded.
1802 + if (err == null) { console.log("Done."); } else { console.log(err); }
1803 + if (exe != null) { exe.close(); }
1804 + });
1805 + return;
1806 }
1807 console.log("Done.");
1808 }
@@ -1612,9 +1879,14 @@ function start() {
1879 if (command == 'timestamp') {
1880 if (exe == null) { console.log("Missing --exe [filename]"); return; }
1881 if (exe.signature == null) { console.log("Executable is not signed."); return; }
1615 - if (typeof args.url != 'string') { console.log("Missing --url [url]"); return; }
1882 + if (typeof args.time != 'string') { console.log("Missing --time [url]"); return; }
1883 + createOutFile(args, args.exe);
1884 console.log("Requesting time signature...");
1617 - exe.timeStampRequest(args.url, function (err, response) { console.log("Done: RSP: " + response); })
1885 + exe.timeStampRequest(args, function (err) {
1886 + if (err == null) { console.log("Done."); } else { console.log(err); }
1887 + if (exe != null) { exe.close(); }
1888 + })
1889 + return;
1890 }
1891
1892 // Close the file