Added authenticode HTTP proxy support, #4168

Ylian Saint-Hilaire committed Jun 23, 2022 at 15:39 UTC 0d3454fb86033fe63c0f2ca1b6c8a3912b53c5a1
4 files changed +126 -49
authenticode.js
+118 -48
@@ -1345,40 +1345,106 @@ function createAuthenticodeHandler(path) {
1345
1346 // Make a HTTP request, use a proxy if needed
1347 function httpRequest(options, requestBody, func) {
1348 - // If needed, decode the URL
1349 - if (options.url) {
1350 - const timeServerUrl = new URL(options.url);
1351 - options.protocol = timeServerUrl.protocol;
1352 - options.hostname = timeServerUrl.hostname;
1353 - options.path = timeServerUrl.pathname;
1354 - options.port = ((timeServerUrl.port == '') ? 80 : parseInt(timeServerUrl.port));
1348 + // Decode the URL
1349 + const timeServerUrl = new URL(options.url);
1350 + options.protocol = timeServerUrl.protocol;
1351 + options.hostname = timeServerUrl.hostname;
1352 + options.path = timeServerUrl.pathname;
1353 + options.port = ((timeServerUrl.port == '') ? 80 : parseInt(timeServerUrl.port));
1354 +
1355 + if (options.proxy == null) {
1356 + // No proxy needed
1357 +
1358 + // Setup the options
1359 delete options.url;
1360 + options.method = 'POST';
1361 + options.headers = {
1362 + 'accept': 'application/octet-stream',
1363 + 'cache-control': 'no-cache',
1364 + 'user-agent': 'Transport',
1365 + 'content-type': 'application/octet-stream',
1366 + 'content-length': Buffer.byteLength(requestBody)
1367 + };
1368 +
1369 + // Set up the request
1370 + var responseAccumulator = '';
1371 + var req = require('http').request(options, function (res) {
1372 + res.setEncoding('utf8');
1373 + res.on('data', function (chunk) { responseAccumulator += chunk; });
1374 + res.on('end', function () { func(null, responseAccumulator); });
1375 + });
1376 +
1377 + // Post the data
1378 + req.on('error', function (err) { func('' + err); });
1379 + req.write(requestBody);
1380 + req.end();
1381 + } else {
1382 + // We are using a proxy
1383 + // This is a fairly basic proxy implementation, should work most of the time.
1384 +
1385 + // Setup the options and decode the proxy URL
1386 + var proxyOptions = { method: 'CONNECT' };
1387 + if (options.proxy) {
1388 + const proxyUrl = new URL(options.proxy);
1389 + proxyOptions.protocol = proxyUrl.protocol;
1390 + proxyOptions.hostname = proxyUrl.hostname;
1391 + proxyOptions.path = options.hostname + ':' + options.port;
1392 + proxyOptions.port = ((proxyUrl.port == '') ? 80 : parseInt(proxyUrl.port));
1393 + }
1394 +
1395 + // Set up the proxy request
1396 + var responseAccumulator = '';
1397 + var req = require('http').request(proxyOptions);
1398 + req.on('error', function (err) { func('' + err); });
1399 + req.on('connect', function (res, socket, head) {
1400 + // Make a request over the HTTP tunnel
1401 + socket.write('POST ' + options.path + ' HTTP/1.1\r\n' +
1402 + 'host: ' + options.hostname + ':' + options.port + '\r\n' +
1403 + 'accept: application/octet-stream\r\n' +
1404 + 'cache-control: no-cache\r\n' +
1405 + 'user-agent: Transport\r\n' +
1406 + 'content-type: application/octet-stream\r\n' +
1407 + 'content-length: ' + Buffer.byteLength(requestBody) + '\r\n' +
1408 + '\r\n' + requestBody);
1409 + socket.on('data', function (chunk) {
1410 + responseAccumulator += chunk.toString();
1411 + var responseData = parseHttpResponse(responseAccumulator);
1412 + if (responseData != null) { try { socket.end(); } catch (ex) { console.log('ex', ex); } socket.xdone = true; func(null, responseData); }
1413 + });
1414 + socket.on('end', function () {
1415 + if (socket.xdone == true) return;
1416 + var responseData = parseHttpResponse(responseAccumulator);
1417 + if (responseData != null) { func(null, responseData); } else { func("Unable to parse response."); }
1418 + });
1419 + });
1420 + req.end();
1421 }
1422 + }
1423
1358 - // Setup the options
1359 - options.method = 'POST';
1360 - options.headers = {
1361 - 'accept': 'application/octet-stream',
1362 - 'cache-control': 'no-cache',
1363 - 'user-agent': 'Transport',
1364 - 'content-type': 'application/octet-stream',
1365 - 'content-length': Buffer.byteLength(requestBody)
1366 - };
1367 -
1368 - // Set up the request
1369 - var responseAccumulator = '';
1370 - var req = require('http').request(options, function (res) {
1371 - res.setEncoding('utf8');
1372 - res.on('data', function (chunk) { responseAccumulator += chunk; });
1373 - res.on('end', function () { func(null, responseAccumulator); });
1374 - });
1424 + // Parse the HTTP response and return data if available
1425 + function parseHttpResponse(data) {
1426 + var dataSplit = data.split('\r\n\r\n');
1427 + if (dataSplit.length < 2) return null;
1428 +
1429 + // Parse the HTTP header
1430 + var headerSplit = dataSplit[0].split('\r\n'), headers = {};
1431 + for (var i in headerSplit) {
1432 + if (i != 0) {
1433 + var x = headerSplit[i].indexOf(':');
1434 + headers[headerSplit[i].substring(0, x).toLowerCase()] = headerSplit[i].substring(x + 2);
1435 + }
1436 + }
1437
1376 - // Post the data
1377 - req.on('error', function (err) { func('' + err); });
1378 - req.write(requestBody);
1379 - req.end();
1438 + // If there is a content-length in the header, keep accumulating data until we have the right length
1439 + if (headers['content-length'] != null) {
1440 + const contentLength = parseInt(headers['content-length']);
1441 + if (dataSplit[1].length < contentLength) return null; // Wait for more data
1442 + return dataSplit[1];
1443 + }
1444 + return dataSplit[1];
1445 }
1446
1447 + // Complete the signature of an executable
1448 function signEx(args, p7signature, filesize, func) {
1449 // Open the output file
1450 var output = null;
@@ -1653,26 +1719,28 @@ function createAuthenticodeHandler(path) {
1719
1720 // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
1721 // TODO: We could look to see if the certificate is already present in the executable
1656 - const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1657 - for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1658 -
1659 - // Get the time signature and add it to the executables PKCS7
1660 - const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1661 - const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1662 - const asn1obj2 =
1663 - asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1664 - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1665 - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
1666 - timeasn1Signature
1667 - ])
1668 - ]);
1669 - pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
1670 -
1671 - // Re-encode the executable signature block
1672 - const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
1673 -
1674 - // Write the file with the signature block
1675 - writeExecutableEx(output, p7signature, written, func);
1722 + try {
1723 + var timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1724 + for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1725 +
1726 + // Get the time signature and add it to the executables PKCS7
1727 + const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1728 + const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1729 + const asn1obj2 =
1730 + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1731 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1732 + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
1733 + timeasn1Signature
1734 + ])
1735 + ]);
1736 + pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
1737 +
1738 + // Re-encode the executable signature block
1739 + const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
1740 +
1741 + // Write the file with the signature block
1742 + writeExecutableEx(output, p7signature, written, func);
1743 + } catch (ex) { func('' + ex); return; } // Something failed
1744 });
1745 }
1746 return;
@@ -1744,6 +1812,7 @@ function start() {
1812 console.log(" --url [url] URL to embbed into signature.");
1813 console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
1814 console.log(" --time [url] The time signing server URL.");
1815 + console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1816 console.log(" unsign: Remove the signature from the executable.");
1817 console.log(" --exe [file] Required executable to un-sign.");
1818 console.log(" --out [file] Resulting executable with signature removed.");
@@ -1760,6 +1829,7 @@ function start() {
1829 console.log(" --exe [file] Required executable to sign.");
1830 console.log(" --out [file] Resulting signed executable.");
1831 console.log(" --time [url] The time signing server URL.");
1832 + console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1833 console.log("");
1834 console.log("Note that certificate PEM files must first have the signing certificate,");
1835 console.log("followed by all certificates that form the trust chain.");
meshcentral-config-schema.json
+1
@@ -102,6 +102,7 @@
102 "agentCoreDumpUsers": { "type": "array", "description": "List of non-administrator users that have access to mesh agent crash dumps." },
103 "agentSignLock": { "type": "boolean", "default": false, "description": "When code signing an agent using authenticode, lock the agent to only allow connection to this server. (This is in testing, the default value will change to true in the future)." },
104 "agentTimeStampServer": { "type": [ "boolean", "string" ], "default": "http://timestamp.comodoca.com/authenticode", "description": "The time stamping server to use when code signing Windows executables. When set to false, the executables are not time stamped." },
105 + "agentTimeStampProxy": { "type": [ "boolean", "string" ], "description": "The HTTP proxy to use when contacting the time stamping server, if false, no proxy is used. By default, the npmproxy value is used." },
106 "ignoreAgentHashCheck": { "type": [ "boolean", "string" ], "default": false, "description": "When true, the agent no longer checked the TLS certificate of the server. This should be used for debugging only. You can also set this to a comma seperated list of IP addresses to ignore, for example: \"192.168.2.100,192.168.1.0/24\"." },
107 "exactPorts": { "type": "boolean", "default": false, "description": "When set to true, MeshCentral will only grab the required TCP listening ports or fail. It will not try to use the next available port of it's busy." },
108 "allowLoginToken": { "type": "boolean", "default": false },
meshcentral.js
+6 -1
@@ -2886,6 +2886,11 @@ function CreateMeshCentralServer(config, args) {
2886 if (args.agenttimestampserver === false) { timeStampUrl = null; }
2887 else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; }
2888
2889 + // Setup the time server proxy
2890 + var timeStampProxy = null;
2891 + if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; }
2892 + else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; }
2893 +
2894 // Setup the pending operations counter
2895 var pendingOperations = 1;
2896
@@ -2972,7 +2977,7 @@ function CreateMeshCentralServer(config, args) {
2977 if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
2978 }
2979
2975 - const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl }; // Shallow clone
2980 + const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
2981 obj.debug('main', "Code signing agent with arguments: " + JSON.stringify(signingArguments));
2982 if (resChanges == false) {
2983 // Sign the agent the simple way, without changing any resources.
sample-config-advanced.json
+1
@@ -39,6 +39,7 @@
39 "_agentCoreDumpUsers": "user1,user2",
40 "_agentSignLock": true,
41 "_agentTimeStampServer": "http://timestamp.digicert.com",
42 + "_agentTimeStampProxy": "http://1.2.3.4:80",
43 "_ignoreAgentHashCheck": true,
44 "_exactPorts": true,
45 "_allowLoginToken": true,