externalsignjob - External Code Signing Job (#6977)

* Moving external call back into meshcentral * Debugging logging * Moved the external call to the callback function * Updated codesigning.md * Move callback invoke of callExternalSignJob outside of err check * change console.log to obj.debug for external sign job call logging * obj debug signing failed using obj.debug and console.error inside callExternalSignJob

Chris Norman committed Apr 21, 2025 at 11:49 UTC 5b974e8226083102ad8bcb90d3870ac0b20c395d
2 files changed +75
docs/docs/meshcentral/codesigning.md
+47
@@ -99,3 +99,50 @@ Now that MeshCentral customizes and signs the agent, you can set that value to a
99 }
100 }
101 ```
102 +
103 +## External Signing Job
104 +
105 +The externalsignjob feature allows you to perform additional operations on the agent after MeshCentral completes its code signing process. This is particularly useful for:
106 +
107 +1. Using hardware security tokens for signing
108 +2. Performing signing on a separate server or cloud host
109 +3. Archiving signed agents
110 +4. Adding additional security measures
111 +
112 +The externalsignjob is called after MeshCentral completes its entire code signing process, including:
113 +- Resource modification
114 +- Digital signature application
115 +- Timestamp application (if configured)
116 +
117 +To use this feature, add the following to your config.json:
118 +
119 +```json
120 +"settings": {
121 + "externalsignjob": "path/to/your/script.bat"
122 +}
123 +```
124 +
125 +The script will receive the path to the agent as its first argument. Here are example scripts:
126 +
127 +### Batch File Example
128 +```batch
129 +@echo off
130 +Echo External Signing Job
131 +signtool sign /tr http://timestamp.sectigo.com /td SHA256 /fd SHA256 /a /v /f path/to/your/signing.cer /csp "eToken Base Cryptographic Provider" /k "[{{MyPassword}}]=PrivateKeyContainerName" "%~1"
132 +```
133 +
134 +### PowerShell Example
135 +```powershell
136 +$file = $args[0]
137 +signtool sign /tr http://timestamp.sectigo.com /td SHA256 /fd SHA256 /a /v /f path/to/your/signing.cer /csp "eToken Base Cryptographic Provider" /k "[{{MyPassword}}]=PrivateKeyContainerName" $file
138 +```
139 +
140 +The externalsignjob can be used for more than just signing. For example, you could:
141 +
142 +1. Archive signed agents to a secure location
143 +2. Upload signed agents to a distribution server
144 +3. Perform additional security checks
145 +4. Add custom metadata or watermarks
146 +5. Integrate with your organization's build pipeline
147 +
148 +Note: The script must return a success exit code (0) for the process to be considered successful. Any non-zero exit code will be treated as a failure and will be logged.
meshcentral.js
+28
@@ -3415,6 +3415,7 @@ function CreateMeshCentralServer(config, args) {
3415 // Failed to sign agent
3416 addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
3417 }
3418 + obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
3419 if (--pendingOperations === 0) { agentSignedFunc.func(); }
3420 }
3421 pendingOperations++;
@@ -3470,7 +3471,10 @@ function CreateMeshCentralServer(config, args) {
3471 }
3472
3473 const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
3474 + signingArguments.resChanges = resChanges;
3475 +
3476 obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
3477 + xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
3478 if (resChanges == false) {
3479 // Sign the agent the simple way, without changing any resources.
3480 originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
@@ -3479,16 +3483,40 @@ function CreateMeshCentralServer(config, args) {
3483 // NOTE: This is experimental and could corupt the agent.
3484 originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
3485 }
3486 +
3487 } else {
3488 // Signed agent is already ok, use it.
3489 originalAgent.close();
3490 }
3491 +
3492 +
3493 }
3494 }
3495
3496 if (--pendingOperations === 0) { func(); }
3497 }
3498
3499 + obj.callExternalSignJob = function (signingArguments) {
3500 + if (obj.config.settings && !obj.config.settings.externalsignjob) {
3501 + return;
3502 + }
3503 + obj.debug('main', "External signing job called for file: " + signingArguments.out);
3504 +
3505 + const { spawnSync } = require('child_process');
3506 +
3507 + const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
3508 + encoding: 'utf-8',
3509 + shell: true,
3510 + stdio: 'inherit'
3511 + });
3512 +
3513 + if (signResult.error || signResult.status !== 0) {
3514 + obj.debug('main', "External signing failed for file: " + signingArguments.out);
3515 + console.error("External signing failed for file: " + signingArguments.out);
3516 + return;
3517 + }
3518 + }
3519 +
3520 // Update the list of available mesh agents
3521 obj.updateMeshAgentsTable = function (domain, func) {
3522 // Check if a custom agent signing certificate is available