├── .gitignore ├── LICENSE ├── authenticode.js ├── package.json ├── pkcs7-modified.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore nodejs things 2 | [Nn]ode_modules/ 3 | [Tt]yping/ 4 | [Ii]mages-spare/ 5 | [Dd]aemon/ 6 | [Bb]in/ 7 | [Aa]gent/ 8 | [Aa]gents/modules_meshcmd_min/ 9 | [Aa]gents/modules_meshcore_min/ 10 | [Aa]gents/meshcmd.min.js 11 | [Aa]gents/meshcore.min.js 12 | [Pp]ublic/translations/ 13 | [Vv]iews/translations/ 14 | [Pp]ublic/*-min.htm 15 | [Vv]iews/*-min.handlebars 16 | meshcentral.db 17 | meshcentral.db.json 18 | mesherrors.txt 19 | package-lock.json 20 | bob.json 21 | .greenlockrc 22 | 23 | ## Ignore Visual Studio temporary files, build results, and 24 | ## files generated by popular Visual Studio add-ons. 25 | ## 26 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 27 | 28 | # User-specific files 29 | *.suo 30 | *.user 31 | *.userosscache 32 | *.sln.docstates 33 | 34 | # User-specific files (MonoDevelop/Xamarin Studio) 35 | *.userprefs 36 | 37 | # Build results 38 | [Dd]ebug/ 39 | [Dd]ebugPublic/ 40 | [Rr]elease/ 41 | [Rr]eleases/ 42 | x64/ 43 | x86/ 44 | bld/ 45 | [Bb]in/ 46 | [Oo]bj/ 47 | [Ll]og/ 48 | 49 | # Visual Studio 2015 cache/options directory 50 | .vs/ 51 | # Uncomment if you have tasks that create the project's static files in wwwroot 52 | #wwwroot/ 53 | 54 | # MSTest test Results 55 | [Tt]est[Rr]esult*/ 56 | [Bb]uild[Ll]og.* 57 | 58 | # NUNIT 59 | *.VisualState.xml 60 | TestResult.xml 61 | 62 | # Build Results of an ATL Project 63 | [Dd]ebugPS/ 64 | [Rr]eleasePS/ 65 | dlldata.c 66 | 67 | # .NET Core 68 | project.lock.json 69 | project.fragment.lock.json 70 | artifacts/ 71 | **/Properties/launchSettings.json 72 | 73 | *_i.c 74 | *_p.c 75 | *_i.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.pch 80 | *.pdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # TODO: Comment the next line if you want to checkin your web deploy settings 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/packages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/packages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/packages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | 209 | # Visual Studio cache files 210 | # files ending in .cache can be ignored 211 | *.[Cc]ache 212 | # but keep track of directories ending in .cache 213 | !*.[Cc]ache/ 214 | 215 | # Others 216 | ClientBin/ 217 | ~$* 218 | *~ 219 | *.dbmdl 220 | *.dbproj.schemaview 221 | *.jfm 222 | *.pfx 223 | *.publishsettings 224 | orleans.codegen.cs 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | 241 | # SQL Server files 242 | *.mdf 243 | *.ldf 244 | *.ndf 245 | 246 | # Business Intelligence projects 247 | *.rdl.data 248 | *.bim.layout 249 | *.bim_*.settings 250 | 251 | # Microsoft Fakes 252 | FakesAssemblies/ 253 | 254 | # GhostDoc plugin setting file 255 | *.GhostDoc.xml 256 | 257 | # Node.js Tools for Visual Studio 258 | .ntvs_analysis.dat 259 | node_modules/ 260 | 261 | # Typescript v1 declaration files 262 | typings/ 263 | 264 | # Visual Studio 6 build log 265 | *.plg 266 | 267 | # Visual Studio 6 workspace options file 268 | *.opt 269 | 270 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 271 | *.vbw 272 | 273 | # Visual Studio LightSwitch build output 274 | **/*.HTMLClient/GeneratedArtifacts 275 | **/*.DesktopClient/GeneratedArtifacts 276 | **/*.DesktopClient/ModelManifest.xml 277 | **/*.Server/GeneratedArtifacts 278 | **/*.Server/ModelManifest.xml 279 | _Pvt_Extensions 280 | 281 | # Paket dependency manager 282 | .paket/paket.exe 283 | paket-files/ 284 | 285 | # FAKE - F# Make 286 | .fake/ 287 | 288 | # JetBrains Rider 289 | .idea/ 290 | *.sln.iml 291 | 292 | # CodeRush 293 | .cr/ 294 | 295 | # Python Tools for Visual Studio (PTVS) 296 | __pycache__/ 297 | *.pyc 298 | 299 | # Cake - Uncomment if you are using it 300 | # tools/** 301 | # !tools/packages.config 302 | 303 | # Telerik's JustMock configuration file 304 | *.jmconfig 305 | 306 | # BizTalk build output 307 | *.btp.cs 308 | *.btm.cs 309 | *.odx.cs 310 | *.xsd.cs 311 | .DS_Store 312 | 313 | # When running mkdocs locally as dev 314 | docs/__pycache__/ 315 | docs/env/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /authenticode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Authenticode parsing 3 | * @author Ylian Saint-Hilaire & Bryan Roe 4 | * @copyright Intel Corporation 2018-2022 5 | * @license Apache-2.0 6 | * @version v0.0.1 7 | */ 8 | 9 | /*jslint node: true */ 10 | /*jshint node: true */ 11 | /*jshint strict:false */ 12 | /*jshint -W097 */ 13 | /*jshint esversion: 6 */ 14 | "use strict"; 15 | 16 | const fs = require('fs'); 17 | const crypto = require('crypto'); 18 | const forge = require('node-forge'); 19 | const pki = forge.pki; 20 | const p7 = require('./pkcs7-modified'); 21 | 22 | // Generate a test self-signed certificate with code signing extension 23 | function createSelfSignedCert(args) { 24 | var keys = pki.rsa.generateKeyPair(2048); 25 | var cert = pki.createCertificate(); 26 | cert.publicKey = keys.publicKey; 27 | cert.serialNumber = (typeof args.serial == 'string')?args.serial:'012345'; // Serial number must always have a single leading '0', otherwise toPEM/fromPEM will not work right. 28 | cert.validity.notBefore = new Date(); 29 | cert.validity.notAfter = new Date(); 30 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10); 31 | var attrs = []; 32 | if (typeof args.cn == 'string') { attrs.push({ name: 'commonName', value: args.cn }); } 33 | if (typeof args.country == 'string') { attrs.push({ name: 'countryName', value: args.country }); } 34 | if (typeof args.state == 'string') { attrs.push({ name: 'ST', value: args.state }); } 35 | if (typeof args.locality == 'string') { attrs.push({ name: 'localityName', value: args.locality }); } 36 | if (typeof args.org == 'string') { attrs.push({ name: 'organizationName', value: args.org }); } 37 | if (typeof args.orgunit == 'string') { attrs.push({ name: 'OU', value: args.orgunit }); } 38 | cert.setSubject(attrs); 39 | cert.setIssuer(attrs); 40 | cert.setExtensions([{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]); 41 | cert.sign(keys.privateKey, forge.md.sha384.create()); 42 | return { cert: cert, key: keys.privateKey, extraCerts: [] }; 43 | } 44 | 45 | // Create the output filename if not already specified 46 | function createOutFile(args, filename) { 47 | if (typeof args.out == 'string') return; 48 | var outputFileName = filename.split('.'); 49 | outputFileName[outputFileName.length - 2] += '-out'; 50 | args.out = outputFileName.join('.'); 51 | } 52 | 53 | // Hash an object 54 | function hashObject(obj) { 55 | if (obj == null) { return null; } 56 | const hash = crypto.createHash('sha384'); 57 | if (Buffer.isBuffer(obj)) { hash.update(obj); } else { hash.update(JSON.stringify(obj)); } 58 | return hash.digest().toString('hex'); 59 | } 60 | 61 | // Load a .bmp file. 62 | function loadBitmap(bitmapFile) { 63 | var bitmapData = null; 64 | try { bitmapData = fs.readFileSync(bitmapFile); } catch (ex) { } 65 | if ((bitmapData == null) || (bitmapData.length < 14) || (bitmapData[0] != 0x42) || (bitmapData[1] != 0x4D)) return null; 66 | return bitmapData.slice(14); 67 | } 68 | 69 | // Load a .ico file. This will load all icons in the file into a icon group object 70 | function loadIcon(iconFile) { 71 | var iconData = null; 72 | try { iconData = fs.readFileSync(iconFile); } catch (ex) { } 73 | if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null; 74 | const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} }; 75 | if (r.resType != 1) return null; 76 | var ptr = 6; 77 | for (var i = 1; i <= r.resCount; i++) { 78 | var icon = {}; 79 | icon.width = iconData[ptr + 0]; 80 | icon.height = iconData[ptr + 1]; 81 | icon.colorCount = iconData[ptr + 2]; 82 | icon.planes = iconData.readUInt16LE(ptr + 4); 83 | icon.bitCount = iconData.readUInt16LE(ptr + 6); 84 | icon.bytesInRes = iconData.readUInt32LE(ptr + 8); 85 | icon.iconCursorId = i; 86 | const offset = iconData.readUInt32LE(ptr + 12); 87 | icon.icon = iconData.slice(offset, offset + icon.bytesInRes); 88 | r.icons[i] = icon; 89 | ptr += 16; 90 | } 91 | return r; 92 | } 93 | 94 | // Load certificates and private key from PEM files 95 | function loadCertificates(pemFileNames) { 96 | var certs = [], keys = []; 97 | if (pemFileNames == null) return; 98 | if (typeof pemFileNames == 'string') { pemFileNames = [pemFileNames]; } 99 | for (var i in pemFileNames) { 100 | try { 101 | // Read certificate 102 | var pem = fs.readFileSync(pemFileNames[i]).toString(); 103 | var pemCerts = pem.split('-----BEGIN CERTIFICATE-----'); 104 | for (var j in pemCerts) { 105 | var k = pemCerts[j].indexOf('-----END CERTIFICATE-----'); 106 | if (k >= 0) { certs.push(pki.certificateFromPem('-----BEGIN CERTIFICATE-----' + pemCerts[j].substring(0, k) + '-----END CERTIFICATE-----')); } 107 | } 108 | var PemKeys = pem.split('-----BEGIN RSA PRIVATE KEY-----'); 109 | for (var j in PemKeys) { 110 | var k = PemKeys[j].indexOf('-----END RSA PRIVATE KEY-----'); 111 | if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN RSA PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END RSA PRIVATE KEY-----')); } 112 | } 113 | PemKeys = pem.split('-----BEGIN PRIVATE KEY-----'); 114 | for (var j in PemKeys) { 115 | var k = PemKeys[j].indexOf('-----END PRIVATE KEY-----'); 116 | if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END PRIVATE KEY-----')); } 117 | } 118 | } catch (ex) { } 119 | } 120 | if ((certs.length == 0) || (keys.length != 1)) return; // No certificates or private keys 121 | var r = { cert: certs[0], key: keys[0], extraCerts: [] } 122 | if (certs.length > 1) { for (var i = 1; i < certs.length; i++) { r.extraCerts.push(certs[i]); } } 123 | return r; 124 | } 125 | 126 | function createAuthenticodeHandler(path) { 127 | const obj = {}; 128 | obj.header = { path: path } 129 | 130 | // Read a file slice 131 | function readFileSlice(start, length) { 132 | var buffer = Buffer.alloc(length); 133 | var len = fs.readSync(obj.fd, buffer, 0, buffer.length, start); 134 | if (len < buffer.length) { buffer = buffer.slice(0, len); } 135 | return buffer; 136 | } 137 | 138 | // Close the file 139 | obj.close = function () { 140 | if (obj.fd == null) return; 141 | fs.closeSync(obj.fd); 142 | delete obj.fd; 143 | } 144 | 145 | // Private OIDS 146 | obj.Oids = { 147 | SPC_INDIRECT_DATA_OBJID: '1.3.6.1.4.1.311.2.1.4', 148 | SPC_STATEMENT_TYPE_OBJID: '1.3.6.1.4.1.311.2.1.11', 149 | SPC_SP_OPUS_INFO_OBJID: '1.3.6.1.4.1.311.2.1.12', 150 | SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.21', 151 | SPC_COMMERCIAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.22', 152 | SPC_MS_JAVA_SOMETHING: '1.3.6.1.4.1.311.15.1', 153 | SPC_PE_IMAGE_DATA_OBJID: '1.3.6.1.4.1.311.2.1.15', 154 | SPC_CAB_DATA_OBJID: '1.3.6.1.4.1.311.2.1.25', 155 | SPC_TIME_STAMP_REQUEST_OBJID: '1.3.6.1.4.1.311.3.2.1', 156 | SPC_SIPINFO_OBJID: '1.3.6.1.4.1.311.2.1.30', 157 | SPC_PE_IMAGE_PAGE_HASHES_V1: '1.3.6.1.4.1.311.2.3.1', 158 | SPC_PE_IMAGE_PAGE_HASHES_V2: '1.3.6.1.4.1.311.2.3.2', 159 | SPC_NESTED_SIGNATURE_OBJID: '1.3.6.1.4.1.311.2.4.1', 160 | SPC_RFC3161_OBJID: '1.3.6.1.4.1.311.3.3.1' 161 | } 162 | 163 | // Open the file and read header information 164 | function openFile() { 165 | if (obj.fd != null) return true; 166 | 167 | // Open the file descriptor 168 | obj.path = path; 169 | try { obj.fd = fs.openSync(path, 'r'); } catch (ex) { return false; } // Unable to open file 170 | obj.stats = fs.fstatSync(obj.fd); 171 | obj.filesize = obj.stats.size; 172 | if (obj.filesize < 64) { obj.close(); return false; } // File too short. 173 | 174 | // Read the DOS header (64 bytes) 175 | var buf = readFileSlice(60, 4); 176 | obj.header.peHeaderLocation = buf.readUInt32LE(0); // The DOS header is 64 bytes long, the last 4 bytes are a pointer to the PE header. 177 | obj.header.peOptionalHeaderLocation = obj.header.peHeaderLocation + 24; // The PE optional header is located just after the PE header which is 24 bytes long. 178 | 179 | // Check file size and signature 180 | if (obj.filesize < (160 + obj.header.peHeaderLocation)) { obj.close(); return false; } // Invalid SizeOfHeaders. 181 | if (readFileSlice(obj.header.peHeaderLocation, 4).toString('hex') != '50450000') { obj.close(); return false; } // Invalid PE header, must start with "PE" (HEX: 50 45 00 00). 182 | 183 | // Read the COFF header 184 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image 185 | var coffHeader = readFileSlice(obj.header.peHeaderLocation + 4, 20) 186 | obj.header.coff = {}; 187 | obj.header.coff.machine = coffHeader.readUInt16LE(0); 188 | obj.header.coff.numberOfSections = coffHeader.readUInt16LE(2); 189 | obj.header.coff.timeDateStamp = coffHeader.readUInt32LE(4); 190 | obj.header.coff.pointerToSymbolTable = coffHeader.readUInt32LE(8); 191 | obj.header.coff.numberOfSymbols = coffHeader.readUInt32LE(12); 192 | obj.header.coff.sizeOfOptionalHeader = coffHeader.readUInt16LE(16); 193 | obj.header.coff.characteristics = coffHeader.readUInt16LE(18); 194 | 195 | // Read the entire PE optional header 196 | var optinalHeader = readFileSlice(obj.header.peOptionalHeaderLocation, obj.header.coff.sizeOfOptionalHeader); 197 | 198 | // Decode the PE optional header standard fields 199 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only 200 | obj.header.peStandard = {}; 201 | obj.header.peStandard.magic = optinalHeader.readUInt16LE(0); 202 | switch (obj.header.peStandard.magic) { // Check magic value 203 | case 0x020B: obj.header.pe32plus = 1; break; 204 | case 0x010B: obj.header.pe32plus = 0; break; 205 | default: { obj.close(); return false; } // Invalid Magic in PE 206 | } 207 | obj.header.peStandard.majorLinkerVersion = optinalHeader[2]; 208 | obj.header.peStandard.minorLinkerVersion = optinalHeader[3]; 209 | obj.header.peStandard.sizeOfCode = optinalHeader.readUInt32LE(4); 210 | obj.header.peStandard.sizeOfInitializedData = optinalHeader.readUInt32LE(8); 211 | obj.header.peStandard.sizeOfUninitializedData = optinalHeader.readUInt32LE(12); 212 | obj.header.peStandard.addressOfEntryPoint = optinalHeader.readUInt32LE(16); 213 | obj.header.peStandard.baseOfCode = optinalHeader.readUInt32LE(20); 214 | if (obj.header.pe32plus == 0) { obj.header.peStandard.baseOfData = optinalHeader.readUInt32LE(24); } 215 | 216 | // Decode the PE optional header windows fields 217 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-windows-specific-fields-image-only 218 | obj.header.peWindows = {} 219 | if (obj.header.pe32plus == 0) { 220 | // 32bit header 221 | //obj.header.peWindows.imageBase = optinalHeader.readUInt32LE(28); 222 | obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32); 223 | obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36); 224 | obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40); 225 | obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42); 226 | obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44); 227 | obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46); 228 | obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48); 229 | obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50); 230 | obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52); 231 | obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56); 232 | obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60); 233 | obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64); 234 | obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68); 235 | obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70); 236 | //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readUInt32LE(72); 237 | //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readUInt32LE(76); 238 | //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readUInt32LE(80); 239 | //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readUInt32LE(84); 240 | obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(88); 241 | obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(92); 242 | } else { 243 | // 64bit header 244 | //obj.header.peWindows.imageBase = optinalHeader.readBigUInt64LE(24); // TODO: readBigUInt64LE is not supported in older NodeJS versions 245 | obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32); 246 | obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36); 247 | obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40); 248 | obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42); 249 | obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44); 250 | obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46); 251 | obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48); 252 | obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50); 253 | obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52); 254 | obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56); 255 | obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60); 256 | obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64); 257 | obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68); 258 | obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70); 259 | //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readBigUInt64LE(72); 260 | //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readBigUInt64LE(80); 261 | //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readBigUInt64LE(88); 262 | //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readBigUInt64LE(96); 263 | obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(104); 264 | obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(108); 265 | } 266 | 267 | // Decode the PE optional header data directories 268 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only 269 | obj.header.dataDirectories = {} 270 | const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes. 271 | obj.header.dataDirectories.exportTable = { addr: optinalHeader.readUInt32LE(96 + pePlusOffset), size: optinalHeader.readUInt32LE(100 + pePlusOffset) }; 272 | obj.header.dataDirectories.importTable = { addr: optinalHeader.readUInt32LE(104 + pePlusOffset), size: optinalHeader.readUInt32LE(108 + pePlusOffset) }; 273 | obj.header.dataDirectories.resourceTable = { addr: optinalHeader.readUInt32LE(112 + pePlusOffset), size: optinalHeader.readUInt32LE(116 + pePlusOffset) }; // Same as .rsrc virtual address & size 274 | obj.header.dataDirectories.exceptionTableAddr = { addr: optinalHeader.readUInt32LE(120 + pePlusOffset), size: optinalHeader.readUInt32LE(124 + pePlusOffset) }; // Same as .pdata virtual address & size 275 | obj.header.dataDirectories.certificateTable = { addr: optinalHeader.readUInt32LE(128 + pePlusOffset), size: optinalHeader.readUInt32LE(132 + pePlusOffset) }; 276 | obj.header.dataDirectories.baseRelocationTable = { addr: optinalHeader.readUInt32LE(136 + pePlusOffset), size: optinalHeader.readUInt32LE(140 + pePlusOffset) }; // Same as .reloc virtual address & size 277 | obj.header.dataDirectories.debug = { addr: optinalHeader.readUInt32LE(144 + pePlusOffset), size: optinalHeader.readUInt32LE(148 + pePlusOffset) }; 278 | // obj.header.dataDirectories.architecture = optinalHeader.readBigUInt64LE(152 + pePlusOffset); // Must be zero 279 | obj.header.dataDirectories.globalPtr = { addr: optinalHeader.readUInt32LE(160 + pePlusOffset), size: optinalHeader.readUInt32LE(164 + pePlusOffset) }; 280 | obj.header.dataDirectories.tLSTable = { addr: optinalHeader.readUInt32LE(168 + pePlusOffset), size: optinalHeader.readUInt32LE(172 + pePlusOffset) }; 281 | obj.header.dataDirectories.loadConfigTable = { addr: optinalHeader.readUInt32LE(176 + pePlusOffset), size: optinalHeader.readUInt32LE(180 + pePlusOffset) }; 282 | obj.header.dataDirectories.boundImport = { addr: optinalHeader.readUInt32LE(184 + pePlusOffset), size: optinalHeader.readUInt32LE(188 + pePlusOffset) }; 283 | obj.header.dataDirectories.iAT = { addr: optinalHeader.readUInt32LE(192 + pePlusOffset), size: optinalHeader.readUInt32LE(196 + pePlusOffset) }; 284 | obj.header.dataDirectories.delayImportDescriptor = { addr: optinalHeader.readUInt32LE(200 + pePlusOffset), size: optinalHeader.readUInt32LE(204 + pePlusOffset) }; 285 | obj.header.dataDirectories.clrRuntimeHeader = { addr: optinalHeader.readUInt32LE(208 + pePlusOffset), size: optinalHeader.readUInt32LE(212 + pePlusOffset) }; 286 | // obj.header.dataDirectories.reserved = optinalHeader.readBigUInt64LE(216 + pePlusOffset); // Must be zero 287 | 288 | // Get the certificate table location and size 289 | obj.header.sigpos = obj.header.dataDirectories.certificateTable.addr; 290 | obj.header.siglen = obj.header.dataDirectories.certificateTable.size 291 | obj.header.signed = ((obj.header.sigpos != 0) && (obj.header.siglen != 0)); 292 | 293 | // The section headers are located after the optional PE header 294 | obj.header.SectionHeadersPtr = obj.header.peOptionalHeaderLocation + obj.header.coff.sizeOfOptionalHeader; 295 | 296 | // Read the sections 297 | obj.header.sections = {}; 298 | for (var i = 0; i < obj.header.coff.numberOfSections; i++) { 299 | var section = {}; 300 | buf = readFileSlice(obj.header.SectionHeadersPtr + (i * 40), 40); 301 | if (buf[0] != 46) { obj.close(); return false; }; // Name of the section must start with a dot. If not, something is wrong. 302 | var sectionName = buf.slice(0, 8).toString().trim('\0'); 303 | var j = sectionName.indexOf('\0'); 304 | if (j >= 0) { sectionName = sectionName.substring(0, j); } // Trim any trailing zeroes 305 | section.ptr = obj.header.SectionHeadersPtr + (i * 40); 306 | section.virtualSize = buf.readUInt32LE(8); 307 | section.virtualAddr = buf.readUInt32LE(12); 308 | section.rawSize = buf.readUInt32LE(16); 309 | section.rawAddr = buf.readUInt32LE(20); 310 | section.relocAddr = buf.readUInt32LE(24); 311 | section.lineNumbers = buf.readUInt32LE(28); 312 | section.relocNumber = buf.readUInt16LE(32); 313 | section.lineNumbersNumber = buf.readUInt16LE(34); 314 | section.characteristics = buf.readUInt32LE(36); 315 | obj.header.sections[sectionName] = section; 316 | } 317 | 318 | // Compute the checkSum value for this file 319 | obj.header.peWindows.checkSumActual = runChecksum(); 320 | 321 | // If there is a .rsrc section, read the resource information and locations 322 | if (obj.header.sections['.rsrc'] != null) { 323 | obj.resources = readResourceTable(obj.header.sections['.rsrc'].rawAddr, 0); // Read all resources recursively 324 | } 325 | 326 | if (obj.header.signed) { 327 | // Read signature block 328 | 329 | // Check if the file size allows for the signature block 330 | if (obj.filesize < (obj.header.sigpos + obj.header.siglen)) { obj.close(); return false; } // Executable file too short to contain the signature block. 331 | 332 | // Remove the padding if needed 333 | var i, pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8); 334 | var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4; 335 | if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); } 336 | 337 | // Decode the signature block and check that it's valid 338 | var pkcs7der = null, valid = false; 339 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw)); } catch (ex) { } 340 | try { valid = ((pkcs7der != null) && (forge.asn1.derToOid(pkcs7der.value[1].value[0].value[2].value[0].value) == "1.3.6.1.4.1.311.2.1.4")); } catch (ex) { } 341 | if (pkcs7der == null) { 342 | // Can't decode the signature 343 | obj.header.sigpos = 0; 344 | obj.header.siglen = 0; 345 | obj.header.signed = false; 346 | } else { 347 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future 348 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" 349 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; 350 | 351 | // Decode the PKCS7 message 352 | var pkcs7 = null, pkcs7content = null; 353 | try { 354 | pkcs7 = p7.messageFromAsn1(pkcs7der); 355 | pkcs7content = pkcs7.rawCapture.content.value[0]; 356 | } catch (ex) { } 357 | 358 | if ((pkcs7 == null) || (pkcs7content == null)) { 359 | // Can't decode the signature 360 | obj.header.sigpos = 0; 361 | obj.header.siglen = 0; 362 | obj.header.signed = false; 363 | } else { 364 | // Verify a PKCS#7 signature 365 | // Verify is not currently supported in node-forge, but if implemented in the future, this code could work. 366 | //var caStore = forge.pki.createCaStore(); 367 | //for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); } 368 | // Return is true if all signatures are valid and chain up to a provided CA 369 | //if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); } 370 | 371 | // Get the signing attributes 372 | obj.signingAttribs = []; 373 | try { 374 | for (var i in pkcs7.rawCapture.authenticatedAttributes) { 375 | if ( 376 | (pkcs7.rawCapture.authenticatedAttributes[i].value != null) && 377 | (pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) && 378 | (pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) && 379 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) && 380 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) && 381 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) && 382 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) && 383 | (forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) { 384 | for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) { 385 | if ( 386 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) && 387 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) && 388 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) && 389 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null) 390 | ) { 391 | var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value; 392 | if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else { 393 | var r = ''; // This string value is in UCS2 format, convert it to a normal string. 394 | for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); } 395 | obj.signingAttribs.push(r); 396 | } 397 | } 398 | } 399 | } 400 | } 401 | } catch (ex) { } 402 | 403 | // Set the certificate chain 404 | obj.certificates = pkcs7.certificates; 405 | 406 | // Set the signature 407 | obj.signature = Buffer.from(pkcs7.rawCapture.signature, 'binary'); 408 | 409 | // Get the file hashing algorithm 410 | var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value); 411 | switch (hashAlgoOid) { 412 | case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; } 413 | case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; } 414 | case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; } 415 | case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; } 416 | case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; } 417 | } 418 | 419 | // Get the signed file hash 420 | obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary') 421 | 422 | // Compute the actual file hash 423 | if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); } 424 | } 425 | } 426 | } 427 | return true; 428 | } 429 | 430 | // Make a timestamp signature request 431 | obj.timeStampRequest = function (args, func) { 432 | // Create the timestamp request in DER format 433 | const asn1 = forge.asn1; 434 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; 435 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; 436 | const asn1obj = 437 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 438 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), 439 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 440 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), 441 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 442 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature.toString('binary')) // Signature here 443 | ]) 444 | ]) 445 | ]); 446 | 447 | // Serialize an ASN.1 object to DER format in Base64 448 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); 449 | 450 | // Make an HTTP request 451 | const options = { url: args.time, proxy: args.proxy }; 452 | 453 | // Make a request to the time server 454 | httpRequest(options, requestBody, function (err, data) { 455 | if (err != null) { func(err); return; } 456 | 457 | // Decode the timestamp signature block 458 | var timepkcs7der = null; 459 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } 460 | 461 | // Decode the executable signature block 462 | var pkcs7der = null; 463 | try { 464 | var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary'))); 465 | 466 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable 467 | // TODO: We could look to see if the certificate is already present in the executable 468 | const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; 469 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } 470 | 471 | // Remove any existing time stamp signatures 472 | var newValues = []; 473 | for (var i in pkcs7der.value[1].value[0].value[4].value[0].value) { 474 | const j = pkcs7der.value[1].value[0].value[4].value[0].value[i]; 475 | if ((j.tagClass != 128) || (j.type != 1)) { newValues.push(j); } // If this is not a time stamp, add it to out new list. 476 | } 477 | pkcs7der.value[1].value[0].value[4].value[0].value = newValues; // Set the new list 478 | 479 | // Get the time signature and add it to the executables PKCS7 480 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; 481 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; 482 | const asn1obj2 = 483 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ 484 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 485 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), 486 | timeasn1Signature 487 | ]) 488 | ]); 489 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); 490 | 491 | // Re-encode the executable signature block 492 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); 493 | 494 | // Open the output file 495 | var output = null; 496 | try { output = fs.openSync(args.out, 'w+'); } catch (ex) { } 497 | if (output == null) return false; 498 | var tmp, written = 0; 499 | var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize; 500 | 501 | // Compute pre-header length and copy that to the new file 502 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 503 | var tmp = readFileSlice(written, preHeaderLen); 504 | fs.writeSync(output, tmp); 505 | written += tmp.length; 506 | 507 | // Quad Align the results, adding padding if necessary 508 | var len = executableSize + p7signature.length; 509 | var padding = (8 - ((len) % 8)) % 8; 510 | 511 | // Write the signature header 512 | var addresstable = Buffer.alloc(8); 513 | addresstable.writeUInt32LE(executableSize); 514 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); 515 | fs.writeSync(output, addresstable); 516 | written += addresstable.length; 517 | 518 | // Copy the rest of the file until the start of the signature block 519 | while ((executableSize - written) > 0) { 520 | tmp = readFileSlice(written, Math.min(executableSize - written, 65536)); 521 | fs.writeSync(output, tmp); 522 | written += tmp.length; 523 | } 524 | 525 | // Write the signature block header and signature 526 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure 527 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length 528 | win.writeUInt16LE(512, 4); // WORD revision 529 | win.writeUInt16LE(2, 6); // WORD type 530 | fs.writeSync(output, win); 531 | fs.writeSync(output, p7signature); 532 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } 533 | written += (p7signature.length + padding + 8); 534 | 535 | // Compute the checksum and write it in the PE header checksum location 536 | var tmp = Buffer.alloc(4); 537 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); 538 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); 539 | 540 | // Close the file 541 | fs.closeSync(output); 542 | 543 | // Indicate we are done 544 | func(null); 545 | } catch (ex) { func('' + ex); return; } 546 | }); 547 | } 548 | 549 | // Read a resource table. 550 | // ptr: The pointer to the start of the resource section 551 | // offset: The offset start of the resource table to read 552 | function readResourceTable(ptr, offset) { 553 | var buf = readFileSlice(ptr + offset, 16); 554 | var r = {}; 555 | r.characteristics = buf.readUInt32LE(0); 556 | r.timeDateStamp = buf.readUInt32LE(4); 557 | r.majorVersion = buf.readUInt16LE(8); 558 | r.minorVersion = buf.readUInt16LE(10); 559 | var numberOfNamedEntries = buf.readUInt16LE(12); 560 | var numberOfIdEntries = buf.readUInt16LE(14); 561 | 562 | r.entries = []; 563 | var totalResources = numberOfNamedEntries + numberOfIdEntries; 564 | //console.log('readResourceTable', offset, 16 + (totalResources) * 8, offset + (16 + (totalResources) * 8)); 565 | for (var i = 0; i < totalResources; i++) { 566 | buf = readFileSlice(ptr + offset + 16 + (i * 8), 8); 567 | var resource = {}; 568 | resource.name = buf.readUInt32LE(0); 569 | var offsetToData = buf.readUInt32LE(4); 570 | if ((resource.name & 0x80000000) != 0) { 571 | var oname = resource.name; 572 | resource.name = readLenPrefixUnicodeString(ptr + (resource.name - 0x80000000)); 573 | //console.log('readResourceName', offset + (oname - 0x80000000), 2 + (resource.name.length * 2), offset + (oname - 0x80000000) + (2 + resource.name.length * 2), resource.name); 574 | } 575 | if ((offsetToData & 0x80000000) != 0) { resource.table = readResourceTable(ptr, offsetToData - 0x80000000); } else { resource.item = readResourceItem(ptr, offsetToData); } 576 | r.entries.push(resource); 577 | } 578 | return r; 579 | } 580 | 581 | // Read a resource item 582 | // ptr: The pointer to the start of the resource section 583 | // offset: The offset start of the resource item to read 584 | function readResourceItem(ptr, offset) { 585 | //console.log('readResourceItem', offset, 16, offset + 16); 586 | var buf = readFileSlice(ptr + offset, 16), r = {}; 587 | r.offsetToData = buf.readUInt32LE(0); 588 | r.size = buf.readUInt32LE(4); 589 | //console.log('readResourceData', r.offsetToData - obj.header.sections['.rsrc'].virtualAddr, r.size, r.offsetToData + r.size - obj.header.sections['.rsrc'].virtualAddr); 590 | r.codePage = buf.readUInt32LE(8); 591 | //r.reserved = buf.readUInt32LE(12); 592 | return r; 593 | } 594 | 595 | // Read a unicode stting that starts with the string length as the first byte. 596 | function readLenPrefixUnicodeString(ptr) { 597 | var nameLen = readFileSlice(ptr, 2).readUInt16LE(0); 598 | var buf = readFileSlice(ptr + 2, nameLen * 2), name = ''; 599 | for (var i = 0; i < nameLen; i++) { name += String.fromCharCode(buf.readUInt16LE(i * 2)); } 600 | return name; 601 | } 602 | 603 | // Generate a complete resource section and pad the section 604 | function generateResourceSection(resources) { 605 | // Call a resursive method the compute the size needed for each element 606 | const resSizes = { tables: 0, items: 0, names: 0, data: 0 }; 607 | getResourceSectionSize(resources, resSizes); 608 | 609 | // Pad the resource section & allocate the buffer 610 | const fileAlign = obj.header.peWindows.fileAlignment 611 | var resSizeTotal = resSizes.tables + resSizes.items + resSizes.names + resSizes.data; 612 | var resNoPadding = resSizeTotal + 4; // TODO: Not sure why this is off by 4 613 | if ((resSizeTotal % fileAlign) != 0) { resSizeTotal += (fileAlign - (resSizeTotal % fileAlign)); } 614 | const resSectionBuffer = Buffer.alloc(resSizeTotal); 615 | 616 | // Write the resource section, calling a recursive method 617 | const resPointers = { tables: 0, items: resSizes.tables, names: resSizes.tables + resSizes.items, data: resSizes.tables + resSizes.items + resSizes.names }; 618 | createResourceSection(resources, resSectionBuffer, resPointers); 619 | //console.log('generateResourceSection', resPointers); 620 | 621 | // Done, return the result 622 | return { size: resNoPadding, data: resSectionBuffer }; 623 | } 624 | 625 | // Return the total size of a resource header, this is a recursive method 626 | function getResourceSectionSize(resources, sizes) { 627 | sizes.tables += (16 + (resources.entries.length * 8)); 628 | for (var i in resources.entries) { 629 | if (typeof resources.entries[i].name == 'string') { 630 | var dataSize = (2 + (resources.entries[i].name.length * 2)); 631 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } 632 | sizes.names += dataSize; 633 | } 634 | if (resources.entries[i].table) { getResourceSectionSize(resources.entries[i].table, sizes); } 635 | else if (resources.entries[i].item) { 636 | sizes.items += 16; 637 | if (resources.entries[i].item.buffer) { 638 | sizes.data += resources.entries[i].item.buffer.length; 639 | } else { 640 | var dataSize = resources.entries[i].item.size; 641 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } 642 | sizes.data += dataSize; 643 | } 644 | } 645 | } 646 | } 647 | 648 | // Write the resource section in the buffer, this is a recursive method 649 | function createResourceSection(resources, buf, resPointers) { 650 | var numberOfNamedEntries = 0, numberOfIdEntries = 0, ptr = resPointers.tables; 651 | //console.log('createResourceSection', resPointers, ptr); 652 | 653 | // Figure out how many items we have to save 654 | for (var i in resources.entries) { 655 | if (typeof resources.entries[i].name == 'string') { numberOfNamedEntries++; } else { numberOfIdEntries++; } 656 | } 657 | 658 | // Move the table pointer forward 659 | resPointers.tables += (16 + (8 * numberOfNamedEntries) + (8 * numberOfIdEntries)); 660 | 661 | // Write the table header 662 | buf.writeUInt32LE(resources.characteristics, ptr); 663 | buf.writeUInt32LE(resources.timeDateStamp, ptr + 4); 664 | buf.writeUInt16LE(resources.majorVersion, ptr + 8); 665 | buf.writeUInt16LE(resources.minorVersion, ptr + 10); 666 | buf.writeUInt16LE(numberOfNamedEntries, ptr + 12); 667 | buf.writeUInt16LE(numberOfIdEntries, ptr + 14); 668 | 669 | // For each table entry, write the entry for it 670 | for (var i in resources.entries) { 671 | // Write the name 672 | var name = resources.entries[i].name; 673 | if (typeof resources.entries[i].name == 'string') { 674 | // Set the pointer to the name 675 | name = resPointers.names + 0x80000000; 676 | 677 | // Write the name length, followed by the name string in unicode 678 | buf.writeUInt16LE(resources.entries[i].name.length, resPointers.names); 679 | for (var j = 0; j < resources.entries[i].name.length; j++) { 680 | buf.writeUInt16LE(resources.entries[i].name.charCodeAt(j), 2 + resPointers.names + (j * 2)); 681 | } 682 | 683 | // Move the names pointer forward, 8 byte align 684 | var dataSize = (2 + (resources.entries[i].name.length * 2)); 685 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } 686 | resPointers.names += dataSize; 687 | } 688 | buf.writeUInt32LE(name, ptr + 16 + (i * 8)); 689 | 690 | // Write the data 691 | var data; 692 | if (resources.entries[i].table) { 693 | // This is a pointer to a table entry 694 | data = resPointers.tables + 0x80000000; 695 | createResourceSection(resources.entries[i].table, buf, resPointers); 696 | } else if (resources.entries[i].item) { 697 | // This is a pointer to a data entry 698 | data = resPointers.items; 699 | 700 | // Write the data 701 | var entrySize = 0; 702 | if (resources.entries[i].item.buffer) { 703 | // Write the data from given buffer 704 | resources.entries[i].item.buffer.copy(buf, resPointers.data, 0, resources.entries[i].item.buffer.length); 705 | entrySize = resources.entries[i].item.buffer.length; 706 | } else { 707 | // Write the data from original file 708 | const actualPtr = (resources.entries[i].item.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + obj.header.sections['.rsrc'].rawAddr; 709 | const tmp = readFileSlice(actualPtr, resources.entries[i].item.size); 710 | tmp.copy(buf, resPointers.data, 0, tmp.length); 711 | entrySize = resources.entries[i].item.size;; 712 | } 713 | 714 | // Write the item entry 715 | buf.writeUInt32LE(resPointers.data + obj.header.sections['.rsrc'].virtualAddr, resPointers.items); // Write the pointer relative to the virtual address 716 | buf.writeUInt32LE(entrySize, resPointers.items + 4); 717 | buf.writeUInt32LE(resources.entries[i].item.codePage, resPointers.items + 8); 718 | buf.writeUInt32LE(resources.entries[i].item.reserved, resPointers.items + 12); 719 | 720 | // Move items pointers forward 721 | resPointers.items += 16; 722 | var dataSize = entrySize; 723 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } 724 | resPointers.data += dataSize; 725 | } 726 | buf.writeUInt32LE(data, ptr + 20 + (i * 8)); 727 | } 728 | } 729 | 730 | // Convert a unicode buffer to a string 731 | function unicodeToString(buf) { 732 | var r = '', c; 733 | for (var i = 0; i < (buf.length / 2) ; i++) { 734 | c = buf.readUInt16LE(i * 2); 735 | if (c != 0) { r += String.fromCharCode(c); } else { return r; } 736 | } 737 | return r; 738 | } 739 | 740 | // Convert a string to a unicode buffer 741 | // Input is a string, a buffer to write to and the offset in the buffer (0 is default). 742 | function stringToUnicode(str, buf, offset) { 743 | if (offset == null) { offset = 0; } 744 | for (var i = 0; i < str.length; i++) { buf.writeInt16LE(str.charCodeAt(i), offset + (i * 2)); } 745 | } 746 | 747 | var resourceDefaultNames = { 748 | 'bitmaps': 2, 749 | 'icon': 3, 750 | 'dialogs': 5, 751 | 'iconGroups': 14, 752 | 'versionInfo': 16, 753 | 'configurationFiles': 24 754 | } 755 | 756 | // Return the raw signature block buffer with padding removed 757 | obj.getRawSignatureBlock = function () { 758 | if ((obj.header.sigpos == 0) || (obj.header.siglen == 0)) return null; 759 | var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8); 760 | var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4; 761 | if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); } 762 | return pkcs7raw; 763 | } 764 | 765 | 766 | // Get bitmaps information from resource 767 | obj.getBitmapInfo = function () { 768 | const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr; 769 | 770 | // Find and parse each icon 771 | const bitmaps = {} 772 | for (var i = 0; i < obj.resources.entries.length; i++) { 773 | if (obj.resources.entries[i].name == resourceDefaultNames.bitmaps) { 774 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { 775 | const bitmapName = obj.resources.entries[i].table.entries[j].name; 776 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; 777 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; 778 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; 779 | bitmaps[bitmapName] = readFileSlice(actualPtr, size); 780 | } 781 | } 782 | } 783 | 784 | return bitmaps; 785 | } 786 | 787 | // Get icon information from resource 788 | obj.getIconInfo = function () { 789 | const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr; 790 | 791 | // Find and parse each icon 792 | const icons = {} 793 | for (var i = 0; i < obj.resources.entries.length; i++) { 794 | if (obj.resources.entries[i].name == resourceDefaultNames.icon) { 795 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { 796 | const iconName = obj.resources.entries[i].table.entries[j].name; 797 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; 798 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; 799 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; 800 | icons[iconName] = readFileSlice(actualPtr, size); 801 | } 802 | } 803 | } 804 | 805 | // Find and parse each icon group 806 | for (var i = 0; i < obj.resources.entries.length; i++) { 807 | if (obj.resources.entries[i].name == resourceDefaultNames.iconGroups) { 808 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { 809 | const groupName = obj.resources.entries[i].table.entries[j].name; 810 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; 811 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; 812 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; 813 | const group = {}; 814 | const groupData = readFileSlice(actualPtr, size); 815 | 816 | // Parse NEWHEADER structure: https://docs.microsoft.com/en-us/windows/win32/menurc/newheader 817 | group.resType = groupData.readUInt16LE(2); 818 | group.resCount = groupData.readUInt16LE(4); 819 | 820 | // Parse many RESDIR structure: https://docs.microsoft.com/en-us/windows/win32/menurc/resdir 821 | group.icons = {}; 822 | for (var p = 6; p < size; p += 14) { 823 | var icon = {} 824 | icon.width = groupData[p]; 825 | icon.height = groupData[p + 1]; 826 | icon.colorCount = groupData[p + 2]; 827 | icon.planes = groupData.readUInt16LE(p + 4); 828 | icon.bitCount = groupData.readUInt16LE(p + 6); 829 | icon.bytesInRes = groupData.readUInt32LE(p + 8); 830 | icon.iconCursorId = groupData.readUInt16LE(p + 12); 831 | icon.icon = icons[icon.iconCursorId]; 832 | group.icons[icon.iconCursorId] = icon; 833 | } 834 | 835 | // Add an icon group 836 | r[groupName] = group; 837 | } 838 | } 839 | } 840 | 841 | return r; 842 | } 843 | 844 | // Set bitmap information 845 | obj.setBitmapInfo = function (bitmapInfo) { 846 | // Delete all bitmaps resources 847 | var resourcesEntries = []; 848 | for (var i = 0; i < obj.resources.entries.length; i++) { 849 | if (obj.resources.entries[i].name != resourceDefaultNames.bitmaps) { 850 | resourcesEntries.push(obj.resources.entries[i]); 851 | } 852 | } 853 | obj.resources.entries = resourcesEntries; 854 | 855 | // Add all bitmap entries 856 | const bitmapEntry = { name: resourceDefaultNames.bitmaps, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; 857 | for (var i in bitmapInfo) { 858 | var name = i; 859 | if (parseInt(i) == name) { name = parseInt(i); } 860 | const bitmapItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: bitmapInfo[i], codePage: 0 } }] } } 861 | bitmapEntry.table.entries.push(bitmapItemEntry); 862 | } 863 | obj.resources.entries.push(bitmapEntry); 864 | 865 | // Sort the resources by name. This is required. 866 | function resSort(a, b) { 867 | if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; } 868 | if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; } 869 | if ((typeof a == 'string') && (typeof b == 'number')) { return -1; } 870 | return 1; 871 | } 872 | const names = []; 873 | for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); } 874 | names.sort(resSort); 875 | var newEntryOrder = []; 876 | for (var i in names) { 877 | for (var j = 0; j < obj.resources.entries.length; j++) { 878 | if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); } 879 | } 880 | } 881 | obj.resources.entries = newEntryOrder; 882 | } 883 | 884 | // Set icon information 885 | obj.setIconInfo = function (iconInfo) { 886 | // Delete all icon and icon groups resources 887 | var resourcesEntries = []; 888 | for (var i = 0; i < obj.resources.entries.length; i++) { 889 | if ((obj.resources.entries[i].name != resourceDefaultNames.icon) && (obj.resources.entries[i].name != resourceDefaultNames.iconGroups)) { 890 | resourcesEntries.push(obj.resources.entries[i]); 891 | } 892 | } 893 | obj.resources.entries = resourcesEntries; 894 | 895 | // Count the icon groups and re-number all icons 896 | var iconGroupCount = 0, nextIconNumber = 1; 897 | for (var i in iconInfo) { 898 | iconGroupCount++; 899 | var xicons = {}; 900 | for (var j in iconInfo[i].icons) { xicons[nextIconNumber++] = iconInfo[i].icons[j]; } 901 | iconInfo[i].icons = xicons; 902 | } 903 | if (iconGroupCount == 0) return; // If there are no icon groups, we are done 904 | 905 | // Add the new icons entry 906 | const iconsEntry = { name: resourceDefaultNames.icon, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; 907 | for (var i in iconInfo) { 908 | for (var j in iconInfo[i].icons) { 909 | var name = j; 910 | if (parseInt(j) == name) { name = parseInt(j); } 911 | const iconItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: iconInfo[i].icons[j].icon, codePage: 0 } }] } } 912 | iconsEntry.table.entries.push(iconItemEntry); 913 | } 914 | } 915 | obj.resources.entries.push(iconsEntry); 916 | 917 | // Add the new icon group entry 918 | const groupEntry = { name: resourceDefaultNames.iconGroups, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; 919 | for (var i in iconInfo) { 920 | // Build icon group struct 921 | var iconCount = 0, p = 6; 922 | for (var j in iconInfo[i].icons) { iconCount++; } 923 | const buf = Buffer.alloc(6 + (iconCount * 14)); 924 | buf.writeUInt16LE(iconInfo[i].resType, 2); 925 | buf.writeUInt16LE(iconCount, 4); 926 | for (var j in iconInfo[i].icons) { 927 | buf[p] = iconInfo[i].icons[j].width; 928 | buf[p + 1] = iconInfo[i].icons[j].height; 929 | buf[p + 2] = iconInfo[i].icons[j].colorCount; 930 | buf.writeUInt16LE(iconInfo[i].icons[j].planes, p + 4); 931 | buf.writeUInt16LE(iconInfo[i].icons[j].bitCount, p + 6); 932 | buf.writeUInt32LE(iconInfo[i].icons[j].bytesInRes, p + 8); 933 | buf.writeUInt16LE(j, p + 12); 934 | p += 14; 935 | } 936 | var name = i; 937 | if (parseInt(i) == name) { name = parseInt(i); } 938 | const groupItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: buf, codePage: 0 } }] } } 939 | groupEntry.table.entries.push(groupItemEntry); 940 | } 941 | obj.resources.entries.push(groupEntry); 942 | 943 | // Sort the resources by name. This is required. 944 | function resSort(a, b) { 945 | if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; } 946 | if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; } 947 | if ((typeof a == 'string') && (typeof b == 'number')) { return -1; } 948 | return 1; 949 | } 950 | const names = []; 951 | for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); } 952 | names.sort(resSort); 953 | var newEntryOrder = []; 954 | for (var i in names) { 955 | for (var j = 0; j < obj.resources.entries.length; j++) { 956 | if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); } 957 | } 958 | } 959 | obj.resources.entries = newEntryOrder; 960 | } 961 | 962 | // Decode the version information from the resource 963 | obj.getVersionInfo = function () { 964 | var r = {}, info = readVersionInfo(getVersionInfoData(), 0); 965 | if ((info == null) || (info.stringFiles == null)) return null; 966 | var StringFileInfo = null; 967 | for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } } 968 | if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return null; 969 | const strings = StringFileInfo.stringTable.strings; 970 | for (var i in strings) { r[strings[i].key] = strings[i].value; } 971 | r['~FileVersion'] = (info.fixedFileInfo.dwFileVersionMS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwFileVersionLS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionLS & 0xFFFF); 972 | r['~ProductVersion'] = (info.fixedFileInfo.dwProductVersionMS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwProductVersionLS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionLS & 0xFFFF); 973 | return r; 974 | } 975 | 976 | // Encode the version information to the resource 977 | obj.setVersionInfo = function (versions) { 978 | // Convert the version information into a string array 979 | const stringArray = []; 980 | for (var i in versions) { if (!i.startsWith('~')) { stringArray.push({ key: i, value: versions[i] }); } } 981 | 982 | // Get the existing version data and switch the strings to the new strings 983 | var r = {}, info = readVersionInfo(getVersionInfoData(), 0); 984 | if ((info == null) || (info.stringFiles == null)) return; 985 | var StringFileInfo = null; 986 | for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } } 987 | if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return; 988 | StringFileInfo.stringTable.strings = stringArray; 989 | 990 | // Set the file version 991 | if (versions['~FileVersion'] != null) { 992 | const FileVersionSplit = versions['~FileVersion'].split('.'); 993 | info.fixedFileInfo.dwFileVersionMS = (parseInt(FileVersionSplit[0]) << 16) + parseInt(FileVersionSplit[1]); 994 | info.fixedFileInfo.dwFileVersionLS = (parseInt(FileVersionSplit[2]) << 16) + parseInt(FileVersionSplit[3]); 995 | } 996 | 997 | // Set the product version 998 | if (versions['~ProductVersion'] != null) { 999 | const ProductVersionSplit = versions['~ProductVersion'].split('.'); 1000 | info.fixedFileInfo.dwProductVersionMS = (parseInt(ProductVersionSplit[0]) << 16) + parseInt(ProductVersionSplit[1]); 1001 | info.fixedFileInfo.dwProductVersionLS = (parseInt(ProductVersionSplit[2]) << 16) + parseInt(ProductVersionSplit[3]); 1002 | } 1003 | 1004 | // Re-encode the version information into a buffer 1005 | var verInfoResBufArray = []; 1006 | writeVersionInfo(verInfoResBufArray, info); 1007 | var verInfoRes = Buffer.concat(verInfoResBufArray); 1008 | 1009 | // Display all buffers 1010 | //console.log('--WRITE BUF ARRAY START--'); 1011 | //for (var i in verInfoResBufArray) { console.log(verInfoResBufArray[i].toString('hex')); } 1012 | //console.log('--WRITE BUF ARRAY END--'); 1013 | //console.log('OUT', Buffer.concat(verInfoResBufArray).toString('hex')); 1014 | 1015 | // Set the new buffer as part of the resources 1016 | for (var i = 0; i < obj.resources.entries.length; i++) { 1017 | if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) { 1018 | const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item; 1019 | delete verInfo.size; 1020 | delete verInfo.offsetToData; 1021 | verInfo.buffer = verInfoRes; 1022 | obj.resources.entries[i].table.entries[0].table.entries[0].item = verInfo; 1023 | } 1024 | } 1025 | } 1026 | 1027 | // Return the version info data block 1028 | function getVersionInfoData() { 1029 | if (obj.resources == null) return null; 1030 | const ptr = obj.header.sections['.rsrc'].rawAddr; 1031 | for (var i = 0; i < obj.resources.entries.length; i++) { 1032 | if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) { 1033 | const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item; 1034 | if (verInfo.buffer != null) { 1035 | return verInfo.buffer; 1036 | } else { 1037 | const actualPtr = (verInfo.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; 1038 | return readFileSlice(actualPtr, verInfo.size); 1039 | } 1040 | } 1041 | } 1042 | return null; 1043 | } 1044 | 1045 | // Create a VS_VERSIONINFO structure as a array of buffer that is ready to be placed in the resource section 1046 | // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo 1047 | function writeVersionInfo(bufArray, info) { 1048 | const buf = Buffer.alloc(40); 1049 | buf.writeUInt16LE(0, 4); // wType 1050 | stringToUnicode('VS_VERSION_INFO', buf, 6); 1051 | bufArray.push(buf); 1052 | 1053 | var wLength = 40; 1054 | var wValueLength = 0; 1055 | if (info.fixedFileInfo != null) { 1056 | const buf2 = Buffer.alloc(52); 1057 | wLength += 52; 1058 | wValueLength += 52; 1059 | buf2.writeUInt32LE(info.fixedFileInfo.dwSignature, 0); // dwSignature 1060 | buf2.writeUInt32LE(info.fixedFileInfo.dwStrucVersion, 4); // dwStrucVersion 1061 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionMS, 8); // dwFileVersionMS 1062 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionLS, 12); // dwFileVersionLS 1063 | buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionMS, 16); // dwProductVersionMS 1064 | buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionLS, 20); // dwProductVersionLS 1065 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlagsMask, 24); // dwFileFlagsMask 1066 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlags, 28); // dwFileFlags 1067 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileOS, 32); // dwFileOS 1068 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileType, 36); // dwFileType 1069 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileSubtype, 40); // dwFileSubtype 1070 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateMS, 44); // dwFileDateMS 1071 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateLS, 48); // dwFileDateLS 1072 | bufArray.push(buf2); 1073 | } 1074 | 1075 | if (info.stringFiles != null) { wLength += writeStringFileInfo(bufArray, info.stringFiles); } 1076 | 1077 | buf.writeUInt16LE(Buffer.concat(bufArray).length, 0); // wLength 1078 | buf.writeUInt16LE(wValueLength, 2); // wValueLength 1079 | return wLength; 1080 | } 1081 | 1082 | // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo 1083 | function writeStringFileInfo(bufArray, stringFiles) { 1084 | var totalLen = 0; 1085 | for (var i in stringFiles) { 1086 | var l = 6 + (stringFiles[i].szKey.length * 2); 1087 | if (stringFiles[i].szKey == 'VarFileInfo') { l += 4; } // TODO: This is a hack, not sure what the correct code should be 1088 | const buf2 = Buffer.alloc(padPointer(l)); 1089 | buf2.writeUInt16LE(1, 4); // wType 1090 | stringToUnicode(stringFiles[i].szKey, buf2, 6); 1091 | bufArray.push(buf2); 1092 | 1093 | var wLength = 0, wValueLength = 0; 1094 | 1095 | if (stringFiles[i].szKey == 'StringFileInfo') { wLength += writeStringTableStruct(bufArray, stringFiles[i].stringTable); } 1096 | if (stringFiles[i].szKey == 'VarFileInfo') { wLength += writeVarFileInfoStruct(bufArray, stringFiles[i].varFileInfo); } 1097 | 1098 | buf2.writeUInt16LE(l + wLength, 0); // wLength 1099 | buf2.writeUInt16LE(wValueLength, 2); // wValueLength 1100 | totalLen += buf2.length + wLength; 1101 | } 1102 | return totalLen; 1103 | } 1104 | 1105 | // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str 1106 | function writeVarFileInfoStruct(bufArray, varFileInfo) { 1107 | var l = 8 + (varFileInfo.szKey.length * 2); 1108 | const buf = Buffer.alloc(padPointer(l)); 1109 | buf.writeUInt16LE(0, 4); // wType 1110 | stringToUnicode(varFileInfo.szKey, buf, 6); 1111 | bufArray.push(buf); 1112 | 1113 | var wLength = 0; 1114 | var wValueLength = 0; 1115 | 1116 | if (varFileInfo.value) { 1117 | bufArray.push(varFileInfo.value); 1118 | wLength += varFileInfo.value.length; 1119 | wValueLength += varFileInfo.value.length; 1120 | } 1121 | buf.writeUInt16LE(buf.length + wLength, 0); // wLength 1122 | buf.writeUInt16LE(wValueLength, 2); // wValueLength 1123 | return buf.length + wLength; 1124 | } 1125 | 1126 | // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable 1127 | function writeStringTableStruct(bufArray, stringTable) { 1128 | //console.log('writeStringTableStruct', stringTable); 1129 | var l = 6 + (stringTable.szKey.length * 2); 1130 | const buf = Buffer.alloc(padPointer(l)); 1131 | buf.writeUInt16LE(1, 4); // wType 1132 | stringToUnicode(stringTable.szKey, buf, 6); 1133 | bufArray.push(buf); 1134 | 1135 | var wLength = 0; 1136 | var wValueLength = 0; 1137 | 1138 | if (stringTable.strings) { wLength += writeStringStructs(bufArray, stringTable.strings); } 1139 | buf.writeUInt16LE(l + wLength, 0); // wLength 1140 | buf.writeUInt16LE(wValueLength, 2); // wValueLength 1141 | return buf.length + wLength; 1142 | } 1143 | 1144 | // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str 1145 | function writeStringStructs(bufArray, stringTable) { 1146 | //console.log('writeStringStructs', stringTable); 1147 | var totalLen = 0, bufadd = 0; 1148 | for (var i in stringTable) { 1149 | //console.log('writeStringStructs', stringTable[i]); 1150 | const buf = Buffer.alloc(padPointer(6 + ((stringTable[i].key.length + 1) * 2))); 1151 | var buf2, wLength = buf.length; 1152 | var wValueLength = 0; 1153 | stringToUnicode(stringTable[i].key, buf, 6); 1154 | bufArray.push(buf); 1155 | bufadd += buf.length; 1156 | if (typeof stringTable[i].value == 'string') { 1157 | // wType (string) 1158 | buf.writeUInt16LE(1, 4); 1159 | var l = (stringTable[i].value.length + 1) * 2; 1160 | buf2 = Buffer.alloc(padPointer(l)); 1161 | stringToUnicode(stringTable[i].value, buf2, 0); 1162 | bufArray.push(buf2); 1163 | bufadd += buf2.length; 1164 | wValueLength = stringTable[i].value.length + 1; 1165 | wLength += l; 1166 | } 1167 | if (typeof stringTable[i].value == 'object') { 1168 | // wType (binary) 1169 | buf.writeUInt16LE(2, 4); // TODO: PADDING 1170 | bufArray.push(stringTable[i].value); 1171 | bufadd += stringTable[i].value.length; 1172 | wValueLength = stringTable[i].value.length; 1173 | wLength += wValueLength; 1174 | } 1175 | buf.writeUInt16LE(wLength, 0); // wLength 1176 | buf.writeUInt16LE(wValueLength, 2); // wValueLength 1177 | //console.log('WStringStruct', buf.toString('hex'), buf2.toString('hex')); 1178 | totalLen += wLength; 1179 | } 1180 | //return totalLen; 1181 | return bufadd; 1182 | } 1183 | 1184 | // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo 1185 | function readVersionInfo(buf, ptr) { 1186 | const r = {}; 1187 | if (buf.length < 2) return null; 1188 | const wLength = buf.readUInt16LE(ptr); 1189 | if (buf.length < wLength) return null; 1190 | const wValueLength = buf.readUInt16LE(ptr + 2); 1191 | const wType = buf.readUInt16LE(ptr + 4); 1192 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 36)); 1193 | if (r.szKey != 'VS_VERSION_INFO') return null; 1194 | //console.log('getVersionInfo', wLength, wValueLength, wType, r.szKey.toString()); 1195 | if (wValueLength == 52) { r.fixedFileInfo = readFixedFileInfoStruct(buf, ptr + 40); } 1196 | r.stringFiles = readStringFilesStruct(buf, ptr + 40 + wValueLength, wLength - 40 - wValueLength); 1197 | return r; 1198 | } 1199 | 1200 | // VS_FIXEDFILEINFO structure: https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo 1201 | function readFixedFileInfoStruct(buf, ptr) { 1202 | if (buf.length - ptr < 50) return null; 1203 | var r = {}; 1204 | r.dwSignature = buf.readUInt32LE(ptr); 1205 | if (r.dwSignature != 0xFEEF04BD) return null; 1206 | r.dwStrucVersion = buf.readUInt32LE(ptr + 4); 1207 | r.dwFileVersionMS = buf.readUInt32LE(ptr + 8); 1208 | r.dwFileVersionLS = buf.readUInt32LE(ptr + 12); 1209 | r.dwProductVersionMS = buf.readUInt32LE(ptr + 16); 1210 | r.dwProductVersionLS = buf.readUInt32LE(ptr + 20); 1211 | r.dwFileFlagsMask = buf.readUInt32LE(ptr + 24); 1212 | r.dwFileFlags = buf.readUInt32LE(ptr + 28); 1213 | r.dwFileOS = buf.readUInt32LE(ptr + 32); 1214 | r.dwFileType = buf.readUInt32LE(ptr + 36); 1215 | r.dwFileSubtype = buf.readUInt32LE(ptr + 40); 1216 | r.dwFileDateMS = buf.readUInt32LE(ptr + 44); 1217 | r.dwFileDateLS = buf.readUInt32LE(ptr + 48); 1218 | return r; 1219 | } 1220 | 1221 | // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo 1222 | function readStringFilesStruct(buf, ptr, len) { 1223 | var t = [], startPtr = ptr; 1224 | while (ptr < (startPtr + len)) { 1225 | const r = {}; 1226 | const wLength = buf.readUInt16LE(ptr); 1227 | if (wLength == 0) return t; 1228 | const wValueLength = buf.readUInt16LE(ptr + 2); 1229 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary 1230 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + (wLength - 6))); // String value 1231 | //console.log('readStringFileStruct', wLength, wValueLength, wType, r.szKey); 1232 | if (r.szKey == 'StringFileInfo') { r.stringTable = readStringTableStruct(buf, ptr + 36); } 1233 | if (r.szKey == 'VarFileInfo') { r.varFileInfo = readVarFileInfoStruct(buf, ptr + 32); } 1234 | t.push(r); 1235 | ptr += wLength; 1236 | ptr = padPointer(ptr); 1237 | } 1238 | return t; 1239 | } 1240 | 1241 | // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str 1242 | function readVarFileInfoStruct(buf, ptr) { 1243 | const r = {}; 1244 | const wLength = buf.readUInt16LE(ptr); 1245 | const wValueLength = buf.readUInt16LE(ptr + 2); 1246 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary 1247 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + wLength)); // "VarFileInfo" 1248 | r.value = buf.slice(ptr + wLength - wValueLength, ptr + wLength) 1249 | //console.log('readVarFileInfoStruct', wLength, wValueLength, wType, r.szKey, r.value.toString('hex')); 1250 | return r; 1251 | } 1252 | 1253 | // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable 1254 | function readStringTableStruct(buf, ptr) { 1255 | const r = {}; 1256 | const wLength = buf.readUInt16LE(ptr); 1257 | const wValueLength = buf.readUInt16LE(ptr + 2); 1258 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary 1259 | //console.log('RStringTableStruct', buf.slice(ptr, ptr + wLength).toString('hex')); 1260 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + 16)); // An 8-digit hexadecimal number stored as a Unicode string. 1261 | //console.log('readStringTableStruct', wLength, wValueLength, wType, r.szKey); 1262 | r.strings = readStringStructs(buf, ptr + 24 + wValueLength, wLength - 22); 1263 | return r; 1264 | } 1265 | 1266 | // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str 1267 | function readStringStructs(buf, ptr, len) { 1268 | var t = [], startPtr = ptr; 1269 | while ((ptr + 6) < (startPtr + len)) { 1270 | const r = {}; 1271 | const wLength = buf.readUInt16LE(ptr); 1272 | if (wLength == 0) return t; 1273 | 1274 | //console.log('RStringStruct', buf.slice(ptr, ptr + wLength).toString('hex')); 1275 | 1276 | const wValueLength = buf.readUInt16LE(ptr + 2); 1277 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary 1278 | 1279 | //console.log('R', buf.slice(ptr, ptr + wLength).toString('hex')); 1280 | 1281 | r.key = unicodeToString(buf.slice(ptr + 6, ptr + (wLength - (wValueLength * 2)) - 2)); // Key 1282 | if (wType == 1) { r.value = unicodeToString(buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength - 2)); } // String value 1283 | if (wType == 2) { r.value = buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength); } // Binary value 1284 | //console.log('readStringStruct', wLength, wValueLength, wType, r.key, r.value); 1285 | t.push(r); 1286 | ptr += wLength; 1287 | ptr = padPointer(ptr); 1288 | } 1289 | return t; 1290 | } 1291 | 1292 | // Return the next 4 byte aligned number 1293 | function padPointer(ptr) { return ptr + (((ptr % 4) == 0) ? 0 : (4 - (ptr % 4))); } 1294 | //function padPointer(ptr) { return ptr + (ptr % 4); } 1295 | 1296 | // Hash the file using the selected hashing system 1297 | // This hash skips the executables CRC and code signing data and signing block 1298 | obj.getHash = function(algo) { 1299 | const hash = crypto.createHash(algo); 1300 | runHash(hash, 0, obj.header.peHeaderLocation + 88); 1301 | runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 1302 | runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize); 1303 | return hash.digest(); 1304 | } 1305 | 1306 | // Hash of an open file using the selected hashing system 1307 | // This hash skips the executables CRC and code signing data and signing block 1308 | obj.getHashOfFile = function(fd, algo, filesize) { 1309 | const hash = crypto.createHash(algo); 1310 | runHashOnFile(fd, hash, 0, obj.header.peHeaderLocation + 88); 1311 | runHashOnFile(fd, hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 1312 | runHashOnFile(fd, hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : filesize); 1313 | return hash.digest(); 1314 | } 1315 | 1316 | // Hash the file using the selected hashing system skipping resource section 1317 | // This hash skips the executables CRC, sections table, resource section, code signing data and signing block 1318 | obj.getHashNoResources = function (algo) { 1319 | if (obj.header.sections['.rsrc'] == null) { return obj.getHash(algo); } // No resources in this executable, return a normal hash 1320 | 1321 | // Get the sections table start and size 1322 | const sectionHeaderPtr = obj.header.SectionHeadersPtr; 1323 | const sectionHeaderSize = obj.header.coff.numberOfSections * 40; 1324 | 1325 | // Get the resource section start and size 1326 | const resPtr = obj.header.sections['.rsrc'].rawAddr; 1327 | const resSize = obj.header.sections['.rsrc'].rawSize; 1328 | 1329 | // Get the end-of-file location 1330 | const eof = obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize; 1331 | 1332 | // Hash the remaining data 1333 | const hash = crypto.createHash(algo); 1334 | runHash(hash, 0, obj.header.peHeaderLocation + 88); 1335 | runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 1336 | runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, sectionHeaderPtr); 1337 | runHash(hash, sectionHeaderPtr + sectionHeaderSize, resPtr); 1338 | runHash(hash, resPtr + resSize, eof); 1339 | return hash.digest(); 1340 | } 1341 | 1342 | // Hash the file using the selected hashing system skipping resource section 1343 | // This hash skips the executables CRC, sections table, resource section, code signing data and signing block 1344 | obj.getHashOfSection = function (algo, sectionName) { 1345 | if (obj.header.sections[sectionName] == null) return null; 1346 | 1347 | // Get the section start and size 1348 | const sectionPtr = obj.header.sections[sectionName].rawAddr; 1349 | const sectionSize = obj.header.sections[sectionName].rawSize; 1350 | 1351 | // Hash the remaining data 1352 | const hash = crypto.createHash(algo); 1353 | runHash(hash, sectionPtr, sectionPtr + sectionSize); 1354 | return hash.digest(); 1355 | } 1356 | 1357 | // Hash the file from start to end loading 64k chunks 1358 | function runHash(hash, start, end) { 1359 | var ptr = start; 1360 | while (ptr < end) { const buf = readFileSlice(ptr, Math.min(65536, end - ptr)); hash.update(buf); ptr += buf.length; } 1361 | } 1362 | 1363 | // Hash the open file loading 64k chunks 1364 | // TODO: Do chunks on this!!! 1365 | function runHashOnFile(fd, hash, start, end) { 1366 | const buf = Buffer.alloc(end - start); 1367 | const len = fs.readSync(fd, buf, 0, buf.length, start); 1368 | if (len != buf.length) { console.log('BAD runHashOnFile'); } 1369 | hash.update(buf); 1370 | } 1371 | 1372 | // Checksum the file loading 64k chunks 1373 | function runChecksum() { 1374 | var ptr = 0, c = createChecksum(((obj.header.peOptionalHeaderLocation + 64) / 4)); 1375 | while (ptr < obj.filesize) { const buf = readFileSlice(ptr, Math.min(65536, obj.filesize - ptr)); c.update(buf); ptr += buf.length; } 1376 | return c.digest(); 1377 | } 1378 | 1379 | // Checksum the open file loading 64k chunks 1380 | function runChecksumOnFile(fd, filesize, checksumLocation) { 1381 | var ptr = 0, c = createChecksum(checksumLocation), buf = Buffer.alloc(65536); 1382 | while (ptr < filesize) { var len = fs.readSync(fd, buf, 0, Math.min(65536, filesize - ptr), ptr); c.update(buf, len); ptr += len; } 1383 | return c.digest(); 1384 | } 1385 | 1386 | // Steaming checksum methods 1387 | // TODO: Works only with files padded to 4 byte. 1388 | function createChecksum(checksumLocation) { 1389 | const obj = { checksum: 0, length: 0 }; 1390 | obj.update = function (data, len) { 1391 | if (!len) { len = data.length; } 1392 | for (var i = 0; i < (len / 4) ; i++) { 1393 | if (((obj.length / 4) + i) == checksumLocation) continue; // Skip PE checksum location 1394 | const dword = data.readUInt32LE(i * 4); 1395 | var checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum; 1396 | var checksumhi = (obj.checksum > 4294967296) ? 1 : 0; 1397 | obj.checksum = checksumlo + dword + checksumhi; 1398 | if (obj.checksum > 4294967296) { 1399 | checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum; 1400 | checksumhi = (obj.checksum > 4294967296) ? 1 : 0; 1401 | obj.checksum = checksumlo + checksumhi; 1402 | } 1403 | } 1404 | obj.length += len; 1405 | } 1406 | obj.digest = function () { 1407 | obj.checksum = (obj.checksum & 0xffff) + (obj.checksum >>> 16); 1408 | obj.checksum = (obj.checksum) + (obj.checksum >>> 16); 1409 | obj.checksum = obj.checksum & 0xffff; 1410 | obj.checksum += obj.length; 1411 | return obj.checksum; 1412 | } 1413 | return obj; 1414 | } 1415 | 1416 | // Compute the PE checksum of an entire file 1417 | function getChecksum(data, checksumLocation) { 1418 | var checksum = 0; 1419 | for (var i = 0; i < (data.length / 4) ; i++) { 1420 | if (i == (checksumLocation / 4)) continue; // Skip PE checksum location 1421 | var dword = data.readUInt32LE(i * 4); 1422 | var checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum; 1423 | var checksumhi = (checksum > 4294967296) ? 1 : 0; 1424 | checksum = checksumlo + dword + checksumhi; 1425 | if (checksum > 4294967296) { 1426 | checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum; 1427 | checksumhi = (checksum > 4294967296) ? 1 : 0; 1428 | checksum = checksumlo + checksumhi; 1429 | } 1430 | } 1431 | checksum = (checksum & 0xffff) + (checksum >>> 16); 1432 | checksum = (checksum) + (checksum >>> 16); 1433 | checksum = checksum & 0xffff; 1434 | checksum += data.length; 1435 | return checksum; 1436 | } 1437 | 1438 | // Sign the file using the certificate and key. If none is specified, generate a dummy one 1439 | obj.sign = function (cert, args, func) { 1440 | if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); } 1441 | 1442 | // Set the hash algorithm hash OID 1443 | var hashOid = null, fileHash = null; 1444 | if (args.hash == null) { args.hash = 'sha384'; } 1445 | if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHash('sha256'); } 1446 | if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHash('sha384'); } 1447 | if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); } 1448 | if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); } 1449 | if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); } 1450 | if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; }; 1451 | 1452 | // Create the signature block 1453 | var xp7 = forge.pkcs7.createSignedData(); 1454 | 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') }] }] }; 1455 | 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())]); 1456 | xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content])); 1457 | xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign. 1458 | xp7.addCertificate(cert.cert); 1459 | 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 1460 | 1461 | // Build authenticated attributes 1462 | var authenticatedAttributes = [ 1463 | { type: forge.pki.oids.contentType, value: forge.pki.oids.data }, 1464 | { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge 1465 | ] 1466 | if ((typeof args.desc == 'string') || (typeof args.url == 'string')) { 1467 | var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] }; 1468 | if (args.desc != null) { // Encode description as big-endian unicode. 1469 | var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString() 1470 | for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); } 1471 | codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] }); 1472 | } 1473 | if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); } 1474 | authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes }); 1475 | } 1476 | 1477 | // Add the signer and sign 1478 | xp7.addSigner({ 1479 | key: cert.key, 1480 | certificate: cert.cert, 1481 | digestAlgorithm: forge.pki.oids.sha384, 1482 | authenticatedAttributes: authenticatedAttributes 1483 | }); 1484 | xp7.sign(); 1485 | var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64'); 1486 | 1487 | if (args.time == null) { 1488 | // Sign the executable without timestamp 1489 | signEx(args, p7signature, obj.filesize, func); 1490 | } else { 1491 | // Decode the signature block 1492 | var pkcs7der = null; 1493 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } 1494 | 1495 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future 1496 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" 1497 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; 1498 | 1499 | // Decode the PKCS7 message 1500 | var pkcs7 = p7.messageFromAsn1(pkcs7der); 1501 | 1502 | // Create the timestamp request in DER format 1503 | const asn1 = forge.asn1; 1504 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; 1505 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; 1506 | const asn1obj = 1507 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1508 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), 1509 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1510 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), 1511 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 1512 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here 1513 | ]) 1514 | ]) 1515 | ]); 1516 | 1517 | // Re-decode the PKCS7 from the executable, this time, no workaround needed 1518 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } 1519 | 1520 | // Serialize an ASN.1 object to DER format in Base64 1521 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); 1522 | 1523 | // Make an HTTP request 1524 | const options = { url: args.time, proxy: args.proxy }; 1525 | 1526 | // Make a request to the time server 1527 | httpRequest(options, requestBody, function (err, data) { 1528 | if (err != null) { func(err); return; } 1529 | 1530 | // Decode the timestamp signature block 1531 | var timepkcs7der = null; 1532 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } 1533 | 1534 | try { 1535 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable 1536 | // TODO: We could look to see if the certificate is already present in the executable 1537 | const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; 1538 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } 1539 | 1540 | // Get the time signature and add it to the executables PKCS7 1541 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; 1542 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; 1543 | const asn1obj2 = 1544 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ 1545 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1546 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), 1547 | timeasn1Signature 1548 | ]) 1549 | ]); 1550 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); 1551 | 1552 | // Re-encode the executable signature block 1553 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); 1554 | 1555 | // Write the file with the signature block 1556 | signEx(args, p7signature, obj.filesize, func); 1557 | } catch (ex) { func('' + ex); } 1558 | }); 1559 | } 1560 | } 1561 | 1562 | // Make a HTTP request, use a proxy if needed 1563 | function httpRequest(options, requestBody, func) { 1564 | // Decode the URL 1565 | const timeServerUrl = new URL(options.url); 1566 | options.protocol = timeServerUrl.protocol; 1567 | options.hostname = timeServerUrl.hostname; 1568 | options.path = timeServerUrl.pathname; 1569 | options.port = ((timeServerUrl.port == '') ? 80 : parseInt(timeServerUrl.port)); 1570 | 1571 | if (options.proxy == null) { 1572 | // No proxy needed 1573 | 1574 | // Setup the options 1575 | delete options.url; 1576 | options.method = 'POST'; 1577 | options.headers = { 1578 | 'accept': 'application/octet-stream', 1579 | 'cache-control': 'no-cache', 1580 | 'user-agent': 'Transport', 1581 | 'content-type': 'application/octet-stream', 1582 | 'content-length': Buffer.byteLength(requestBody) 1583 | }; 1584 | 1585 | // Set up the request 1586 | var responseAccumulator = ''; 1587 | var req = require('http').request(options, function (res) { 1588 | res.setEncoding('utf8'); 1589 | res.on('data', function (chunk) { responseAccumulator += chunk; }); 1590 | res.on('end', function () { func(null, responseAccumulator); }); 1591 | }); 1592 | 1593 | // Post the data 1594 | req.on('error', function (err) { func('' + err); }); 1595 | req.write(requestBody); 1596 | req.end(); 1597 | } else { 1598 | // We are using a proxy 1599 | // This is a fairly basic proxy implementation, should work most of the time. 1600 | 1601 | // Setup the options and decode the proxy URL 1602 | var proxyOptions = { method: 'CONNECT' }; 1603 | if (options.proxy) { 1604 | const proxyUrl = new URL(options.proxy); 1605 | proxyOptions.protocol = proxyUrl.protocol; 1606 | proxyOptions.hostname = proxyUrl.hostname; 1607 | proxyOptions.path = options.hostname + ':' + options.port; 1608 | proxyOptions.port = ((proxyUrl.port == '') ? 80 : parseInt(proxyUrl.port)); 1609 | } 1610 | 1611 | // Set up the proxy request 1612 | var responseAccumulator = ''; 1613 | var req = require('http').request(proxyOptions); 1614 | req.on('error', function (err) { func('' + err); }); 1615 | req.on('connect', function (res, socket, head) { 1616 | // Make a request over the HTTP tunnel 1617 | socket.write('POST ' + options.path + ' HTTP/1.1\r\n' + 1618 | 'host: ' + options.hostname + ':' + options.port + '\r\n' + 1619 | 'accept: application/octet-stream\r\n' + 1620 | 'cache-control: no-cache\r\n' + 1621 | 'user-agent: Transport\r\n' + 1622 | 'content-type: application/octet-stream\r\n' + 1623 | 'content-length: ' + Buffer.byteLength(requestBody) + '\r\n' + 1624 | '\r\n' + requestBody); 1625 | socket.on('data', function (chunk) { 1626 | responseAccumulator += chunk.toString(); 1627 | var responseData = parseHttpResponse(responseAccumulator); 1628 | if (responseData != null) { try { socket.end(); } catch (ex) { console.log('ex', ex); } socket.xdone = true; func(null, responseData); } 1629 | }); 1630 | socket.on('end', function () { 1631 | if (socket.xdone == true) return; 1632 | var responseData = parseHttpResponse(responseAccumulator); 1633 | if (responseData != null) { func(null, responseData); } else { func("Unable to parse response."); } 1634 | }); 1635 | }); 1636 | req.end(); 1637 | } 1638 | } 1639 | 1640 | // Parse the HTTP response and return data if available 1641 | function parseHttpResponse(data) { 1642 | var dataSplit = data.split('\r\n\r\n'); 1643 | if (dataSplit.length < 2) return null; 1644 | 1645 | // Parse the HTTP header 1646 | var headerSplit = dataSplit[0].split('\r\n'), headers = {}; 1647 | for (var i in headerSplit) { 1648 | if (i != 0) { 1649 | var x = headerSplit[i].indexOf(':'); 1650 | headers[headerSplit[i].substring(0, x).toLowerCase()] = headerSplit[i].substring(x + 2); 1651 | } 1652 | } 1653 | 1654 | // If there is a content-length in the header, keep accumulating data until we have the right length 1655 | if (headers['content-length'] != null) { 1656 | const contentLength = parseInt(headers['content-length']); 1657 | if (dataSplit[1].length < contentLength) return null; // Wait for more data 1658 | return dataSplit[1]; 1659 | } 1660 | return dataSplit[1]; 1661 | } 1662 | 1663 | // Complete the signature of an executable 1664 | function signEx(args, p7signature, filesize, func) { 1665 | // Open the output file 1666 | var output = null; 1667 | try { output = fs.openSync(args.out, 'w+'); } catch (ex) { } 1668 | if (output == null) { func('Unable to open output file: ' + args.out); return; } 1669 | var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize; 1670 | 1671 | // Compute pre-header length and copy that to the new file 1672 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 1673 | var tmp = readFileSlice(written, preHeaderLen); 1674 | fs.writeSync(output, tmp); 1675 | written += tmp.length; 1676 | 1677 | // Quad Align the results, adding padding if necessary 1678 | var len = executableSize + p7signature.length; 1679 | var padding = (8 - ((len) % 8)) % 8; 1680 | 1681 | // Write the signature header 1682 | var addresstable = Buffer.alloc(8); 1683 | addresstable.writeUInt32LE(executableSize); 1684 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); 1685 | fs.writeSync(output, addresstable); 1686 | written += addresstable.length; 1687 | 1688 | // Copy the rest of the file until the start of the signature block 1689 | while ((executableSize - written) > 0) { 1690 | tmp = readFileSlice(written, Math.min(executableSize - written, 65536)); 1691 | fs.writeSync(output, tmp); 1692 | written += tmp.length; 1693 | } 1694 | 1695 | // Write the signature block header and signature 1696 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure 1697 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length 1698 | win.writeUInt16LE(512, 4); // WORD revision 1699 | win.writeUInt16LE(2, 6); // WORD type 1700 | fs.writeSync(output, win); 1701 | fs.writeSync(output, p7signature); 1702 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } 1703 | written += (p7signature.length + padding + 8); 1704 | 1705 | // Compute the checksum and write it in the PE header checksum location 1706 | var tmp = Buffer.alloc(4); 1707 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); 1708 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); 1709 | 1710 | // Close the file 1711 | fs.closeSync(output); 1712 | func(null); 1713 | } 1714 | 1715 | // Save an executable without the signature 1716 | obj.unsign = function (args) { 1717 | // Open the file 1718 | var output = fs.openSync(args.out, 'w+'); 1719 | var written = 0, totalWrite = obj.header.sigpos; 1720 | 1721 | // Compute pre-header length and copy that to the new file 1722 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 1723 | var tmp = readFileSlice(written, preHeaderLen); 1724 | fs.writeSync(output, tmp); 1725 | written += tmp.length; 1726 | 1727 | // Write the new signature header 1728 | fs.writeSync(output, Buffer.alloc(8)); 1729 | written += 8; 1730 | 1731 | // Copy the rest of the file until the start of the signature block 1732 | while ((totalWrite - written) > 0) { 1733 | tmp = readFileSlice(written, Math.min(totalWrite - written, 65536)); 1734 | fs.writeSync(output, tmp); 1735 | written += tmp.length; 1736 | } 1737 | 1738 | // Compute the checksum and write it in the PE checksum header at position 1739 | var tmp = Buffer.alloc(4); 1740 | tmp.writeUInt32LE(runChecksumOnFile(output, written)); 1741 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); 1742 | 1743 | fs.closeSync(output); 1744 | } 1745 | 1746 | // Find where a directory value is in the old sections and map it to the new sections 1747 | function correctDirectoryValue(oldSections, newSections, value) { 1748 | for (var i in oldSections) { 1749 | if ((value >= oldSections[i].virtualAddr) && (value < (oldSections[i].virtualAddr + oldSections[i].virtualSize))) { 1750 | return newSections[i].virtualAddr + (value - oldSections[i].virtualAddr); 1751 | } 1752 | } 1753 | return 0; 1754 | } 1755 | 1756 | // Save the executable 1757 | obj.writeExecutable = function (args, cert, func) { 1758 | // Open the file 1759 | var output = fs.openSync(args.out, 'w+'); 1760 | var tmp, written = 0; 1761 | 1762 | // Compute the size of the complete executable header up to after the sections header 1763 | var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40); 1764 | var fullHeader = readFileSlice(written, fullHeaderLen); 1765 | 1766 | // Create the resource section 1767 | const rsrcSectionX = generateResourceSection(obj.resources); // This section is created with padding already included 1768 | var rsrcSection = rsrcSectionX.data; 1769 | var rsrcSectionVirtualSize = rsrcSectionX.size; 1770 | var rsrcSectionRawSize = rsrcSection.length; 1771 | 1772 | // Calculate the location and original and new size of the resource segment 1773 | var fileAlign = obj.header.peWindows.fileAlignment 1774 | var resPtr = obj.header.sections['.rsrc'].rawAddr; 1775 | var oldResSize = obj.header.sections['.rsrc'].rawSize; 1776 | var newResSize = rsrcSection.length; 1777 | var resDeltaSize = newResSize - oldResSize; 1778 | 1779 | // Compute the sizeOfInitializedData 1780 | var sizeOfInitializedData = 0; 1781 | for (var i in obj.header.sections) { 1782 | if (i != '.text') { 1783 | if (i == '.rsrc') { 1784 | sizeOfInitializedData += rsrcSectionRawSize; 1785 | } else { 1786 | sizeOfInitializedData += obj.header.sections[i].rawSize; 1787 | } 1788 | } 1789 | } 1790 | 1791 | // Change PE optional header sizeOfInitializedData standard field 1792 | fullHeader.writeUInt32LE(sizeOfInitializedData, obj.header.peOptionalHeaderLocation + 8); 1793 | 1794 | // Update the checksum to zero 1795 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 64); 1796 | 1797 | // We are going to setup the old a new sections here, we need this to correct directory values 1798 | var oldSections = obj.header.sections; 1799 | var newSections = {}; 1800 | 1801 | // Make changes to the segments table 1802 | var virtualAddress = 4096; 1803 | for (var i in obj.header.sections) { 1804 | const section = obj.header.sections[i]; 1805 | newSections[i] = { virtualSize: section.virtualSize }; 1806 | if (i == '.rsrc') { 1807 | // Change the size of the resource section 1808 | fullHeader.writeUInt32LE(rsrcSectionVirtualSize, section.ptr + 8); // virtualSize 1809 | fullHeader.writeUInt32LE(rsrcSectionRawSize, section.ptr + 16); // rawSize 1810 | 1811 | // Set the virtual address of the section 1812 | fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address 1813 | newSections[i].virtualAddr = virtualAddress; 1814 | var virtualAddressPadding = (rsrcSectionVirtualSize % 4096); 1815 | virtualAddress += rsrcSectionVirtualSize; 1816 | if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); } 1817 | } else { 1818 | // Change the location of any other section if located after the resource section 1819 | if (section.rawAddr > resPtr) { fullHeader.writeUInt32LE(section.rawAddr + resDeltaSize, section.ptr + 20); } 1820 | 1821 | // Set the virtual address of the section 1822 | fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address 1823 | newSections[i].virtualAddr = virtualAddress; 1824 | var virtualAddressPadding = (section.virtualSize % 4096); 1825 | virtualAddress += section.virtualSize; 1826 | if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); } 1827 | } 1828 | } 1829 | 1830 | // Make change to the data directories header to fix resource segment size and add/remove signature 1831 | const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes. 1832 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exportTable.addr), obj.header.peOptionalHeaderLocation + 96 + pePlusOffset); 1833 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.importTable.addr), obj.header.peOptionalHeaderLocation + 104 + pePlusOffset); 1834 | fullHeader.writeUInt32LE(rsrcSectionVirtualSize, obj.header.peOptionalHeaderLocation + 116 + pePlusOffset); // Change the resource segment size 1835 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exceptionTableAddr.addr), obj.header.peOptionalHeaderLocation + 120 + pePlusOffset); 1836 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 128 + pePlusOffset); // certificate table addr (TODO) 1837 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 132 + pePlusOffset); // certificate table size (TODO) 1838 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.baseRelocationTable.addr), obj.header.peOptionalHeaderLocation + 136 + pePlusOffset); 1839 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.debug.addr), obj.header.peOptionalHeaderLocation + 144 + pePlusOffset); 1840 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.globalPtr.addr), obj.header.peOptionalHeaderLocation + 160 + pePlusOffset); 1841 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.tLSTable.addr), obj.header.peOptionalHeaderLocation + 168 + pePlusOffset); 1842 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.loadConfigTable.addr), obj.header.peOptionalHeaderLocation + 176 + pePlusOffset); 1843 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.boundImport.addr), obj.header.peOptionalHeaderLocation + 184 + pePlusOffset); 1844 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.iAT.addr), obj.header.peOptionalHeaderLocation + 192 + pePlusOffset); 1845 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.delayImportDescriptor.addr), obj.header.peOptionalHeaderLocation + 200 + pePlusOffset); 1846 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.clrRuntimeHeader.addr), obj.header.peOptionalHeaderLocation + 208 + pePlusOffset); 1847 | 1848 | // Write size of image. We put the next virtual address. 1849 | fullHeader.writeUInt32LE(virtualAddress, obj.header.peOptionalHeaderLocation + 56); // sizeOfImage 1850 | 1851 | // Write the entire header to the destination file 1852 | //console.log('Write header', fullHeader.length, written); 1853 | fs.writeSync(output, fullHeader); 1854 | written += fullHeader.length; 1855 | 1856 | // Write the entire executable until the start to the resource segment 1857 | var totalWrite = resPtr; 1858 | //console.log('Write until res', totalWrite, written); 1859 | while ((totalWrite - written) > 0) { 1860 | tmp = readFileSlice(written, Math.min(totalWrite - written, 65536)); 1861 | fs.writeSync(output, tmp); 1862 | written += tmp.length; 1863 | } 1864 | 1865 | // Write the new resource section 1866 | fs.writeSync(output, rsrcSection); 1867 | written += rsrcSection.length; 1868 | //console.log('Write res', rsrcSection.length, written); 1869 | 1870 | // Write until the signature block 1871 | if (obj.header.sigpos > 0) { 1872 | // Since the original file was signed, write from the end of the resources to the start of the signature block. 1873 | totalWrite = obj.header.sigpos + resDeltaSize; 1874 | } else { 1875 | // The original file was not signed, write from the end of the resources to the end of the file. 1876 | totalWrite = obj.filesize + resDeltaSize; 1877 | } 1878 | 1879 | //console.log('Write until signature', totalWrite, written); 1880 | while ((totalWrite - written) > 0) { 1881 | tmp = readFileSlice(written - resDeltaSize, Math.min(totalWrite - written, 65536)); 1882 | fs.writeSync(output, tmp); 1883 | written += tmp.length; 1884 | } 1885 | //console.log('Write to signature', written); 1886 | 1887 | // Write the signature if needed 1888 | if (cert != null) { 1889 | //if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); } 1890 | 1891 | // Set the hash algorithm hash OID 1892 | var hashOid = null, fileHash = null; 1893 | if (args.hash == null) { args.hash = 'sha384'; } 1894 | if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHashOfFile(output, 'sha256', written); } 1895 | if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHashOfFile(output, 'sha384', written); } 1896 | if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHashOfFile(output, 'sha512', written); } 1897 | if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHashOfFile(output, 'sha224', written); } 1898 | if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHashOfFile(output, 'md5', written); } 1899 | if (hashOid == null) { func('Bad hash method OID'); return; } 1900 | 1901 | // Create the signature block 1902 | var xp7 = forge.pkcs7.createSignedData(); 1903 | 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') }] }] }; 1904 | 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())]); 1905 | xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content])); 1906 | xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign. 1907 | xp7.addCertificate(cert.cert); 1908 | 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 1909 | 1910 | // Build authenticated attributes 1911 | var authenticatedAttributes = [ 1912 | { type: forge.pki.oids.contentType, value: forge.pki.oids.data }, 1913 | { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge 1914 | ] 1915 | if ((typeof args.desc == 'string') || (typeof args.url == 'string')) { 1916 | var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] }; 1917 | if (args.desc != null) { // Encode description as big-endian unicode. 1918 | var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString() 1919 | for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); } 1920 | codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] }); 1921 | } 1922 | if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); } 1923 | authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes }); 1924 | } 1925 | 1926 | // Add the signer and sign 1927 | xp7.addSigner({ 1928 | key: cert.key, 1929 | certificate: cert.cert, 1930 | digestAlgorithm: forge.pki.oids.sha384, 1931 | authenticatedAttributes: authenticatedAttributes 1932 | }); 1933 | xp7.sign(); 1934 | var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64'); 1935 | //console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64')); 1936 | 1937 | if (args.time == null) { 1938 | // Write the signature block to the output executable without time stamp 1939 | writeExecutableEx(output, p7signature, written, func); 1940 | } else { 1941 | // Decode the signature block 1942 | var pkcs7der = null; 1943 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } 1944 | 1945 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future 1946 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" 1947 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; 1948 | 1949 | // Decode the PKCS7 message 1950 | var pkcs7 = p7.messageFromAsn1(pkcs7der); 1951 | 1952 | // Create the timestamp request in DER format 1953 | const asn1 = forge.asn1; 1954 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; 1955 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; 1956 | const asn1obj = 1957 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1958 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), 1959 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1960 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), 1961 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 1962 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here 1963 | ]) 1964 | ]) 1965 | ]); 1966 | 1967 | // Re-decode the PKCS7 from the executable, this time, no workaround needed 1968 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } 1969 | 1970 | // Serialize an ASN.1 object to DER format in Base64 1971 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); 1972 | 1973 | // Make an HTTP request 1974 | const options = { url: args.time, proxy: args.proxy }; 1975 | 1976 | // Make a request to the time server 1977 | httpRequest(options, requestBody, function (err, data) { 1978 | if (err != null) { func(err); return; } 1979 | 1980 | // Decode the timestamp signature block 1981 | var timepkcs7der = null; 1982 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } 1983 | 1984 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable 1985 | // TODO: We could look to see if the certificate is already present in the executable 1986 | try { 1987 | var timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; 1988 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } 1989 | 1990 | // Get the time signature and add it to the executables PKCS7 1991 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; 1992 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; 1993 | const asn1obj2 = 1994 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ 1995 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1996 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), 1997 | timeasn1Signature 1998 | ]) 1999 | ]); 2000 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); 2001 | 2002 | // Re-encode the executable signature block 2003 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); 2004 | 2005 | // Write the file with the signature block 2006 | writeExecutableEx(output, p7signature, written, func); 2007 | } catch (ex) { func('' + ex); return; } // Something failed 2008 | }); 2009 | } 2010 | return; 2011 | } 2012 | 2013 | // Close the file 2014 | fs.closeSync(output); 2015 | 2016 | // Indicate success 2017 | func(null); 2018 | } 2019 | 2020 | function writeExecutableEx(output, p7signature, written, func) { 2021 | // Quad Align the results, adding padding if necessary 2022 | var len = written + p7signature.length; 2023 | var padding = (8 - ((len) % 8)) % 8; 2024 | 2025 | // Write the signature block header and signature 2026 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure 2027 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length 2028 | win.writeUInt16LE(512, 4); // WORD revision 2029 | win.writeUInt16LE(2, 6); // WORD type 2030 | fs.writeSync(output, win); 2031 | fs.writeSync(output, p7signature); 2032 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } 2033 | 2034 | // Write the signature header 2035 | var addresstable = Buffer.alloc(8); 2036 | addresstable.writeUInt32LE(written); 2037 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); 2038 | var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); 2039 | fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation); 2040 | written += (p7signature.length + padding + 8); // Add the signature block to written counter 2041 | 2042 | // Compute the checksum and write it in the PE header checksum location 2043 | var tmp = Buffer.alloc(4); 2044 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); 2045 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); 2046 | 2047 | // Close the file 2048 | fs.closeSync(output); 2049 | 2050 | // Indicate success 2051 | func(null); 2052 | } 2053 | 2054 | // Return null if we could not open the file 2055 | return (openFile() ? obj : null); 2056 | } 2057 | 2058 | function start() { 2059 | // Parse the arguments 2060 | const args = require('minimist')(process.argv.slice(2)); 2061 | 2062 | // Show tool help 2063 | if (process.argv.length < 3) { 2064 | console.log("MeshCentral Authenticode Tool."); 2065 | console.log("Usage:"); 2066 | console.log(" node authenticode.js [command] [options]"); 2067 | console.log("Commands:"); 2068 | console.log(" info: Show information about an executable."); 2069 | console.log(" --exe [file] Required executable to view information."); 2070 | console.log(" --json Show information in JSON format."); 2071 | console.log(" sign: Sign an executable."); 2072 | console.log(" --exe [file] Required executable to sign."); 2073 | console.log(" --out [file] Resulting signed executable."); 2074 | console.log(" --pem [pemfile] Certificate & private key to sign the executable with."); 2075 | console.log(" --desc [description] Description string to embbed into signature."); 2076 | console.log(" --url [url] URL to embbed into signature."); 2077 | console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512."); 2078 | console.log(" --time [url] The time signing server URL."); 2079 | console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://"); 2080 | console.log(" unsign: Remove the signature from the executable."); 2081 | console.log(" --exe [file] Required executable to un-sign."); 2082 | console.log(" --out [file] Resulting executable with signature removed."); 2083 | console.log(" createcert: Create a code signging self-signed certificate and key."); 2084 | console.log(" --out [pemfile] Required certificate file to create."); 2085 | console.log(" --cn [value] Required certificate common name."); 2086 | console.log(" --country [value] Certificate country name."); 2087 | console.log(" --state [value] Certificate state name."); 2088 | console.log(" --locality [value] Certificate locality name."); 2089 | console.log(" --org [value] Certificate organization name."); 2090 | console.log(" --ou [value] Certificate organization unit name."); 2091 | console.log(" --serial [value] Certificate serial number."); 2092 | console.log(" timestamp: Add a signed timestamp to an already signed executable."); 2093 | console.log(" --exe [file] Required executable to timestamp."); 2094 | console.log(" --out [file] Resulting signed executable."); 2095 | console.log(" --time [url] The time signing server URL."); 2096 | console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://"); 2097 | console.log(" bitmaps: Show bitmap resources in the executable."); 2098 | console.log(" --exe [file] Input executable."); 2099 | console.log(" savebitmap: Save a single bitmap to a .bmp file."); 2100 | console.log(" --exe [file] Input executable."); 2101 | console.log(" --out [file] Resulting .ico file."); 2102 | console.log(" --bitmap [number] Bitmap number to save to file."); 2103 | console.log(" icons: Show the icon resources in the executable."); 2104 | console.log(" --exe [file] Input executable."); 2105 | console.log(" saveicon: Save a single icon bitmap to a .ico file."); 2106 | console.log(" --exe [file] Input executable."); 2107 | console.log(" --out [file] Resulting .ico file."); 2108 | console.log(" --icon [number] Icon number to save to file."); 2109 | console.log(" saveicons: Save an icon group to a .ico file."); 2110 | console.log(" --exe [file] Input executable."); 2111 | console.log(" --out [file] Resulting .ico file."); 2112 | console.log(" --icongroup [groupNumber] Icon groupnumber to save to file."); 2113 | console.log(""); 2114 | console.log("Note that certificate PEM files must first have the signing certificate,"); 2115 | console.log("followed by all certificates that form the trust chain."); 2116 | console.log(""); 2117 | console.log("When doing sign/unsign, you can also change resource properties of the generated file."); 2118 | console.log(""); 2119 | console.log(" --fileversionnumber n.n.n.n"); 2120 | console.log(" --productversionnumber n.n.n.n"); 2121 | console.log(" --filedescription [value]"); 2122 | console.log(" --fileversion [value]"); 2123 | console.log(" --internalname [value]"); 2124 | console.log(" --legalcopyright [value]"); 2125 | console.log(" --originalfilename [value]"); 2126 | console.log(" --productname [value]"); 2127 | console.log(" --productversion [value]"); 2128 | console.log(" --removeicongroup [number]"); 2129 | console.log(" --removebitmap [number]"); 2130 | console.log(" --icon [groupNumber],[filename.ico]"); 2131 | console.log(" --bitmap [number],[filename.bmp]"); 2132 | return; 2133 | } 2134 | 2135 | // Check that a valid command is passed in 2136 | if (['info', 'sign', 'unsign', 'createcert', 'icons', 'bitmaps', 'saveicon', 'saveicons', 'savebitmap', 'header', 'sections', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) { 2137 | console.log("Invalid command: " + process.argv[2]); 2138 | console.log("Valid commands are: info, sign, unsign, createcert, timestamp"); 2139 | return; 2140 | } 2141 | 2142 | var exe = null; 2143 | if (args.exe) { 2144 | // Check the file exists and open the file 2145 | var stats = null; 2146 | try { stats = require('fs').statSync(args.exe); } catch (ex) { } 2147 | if (stats == null) { console.log("Unable to executable open file: " + args.exe); return; } 2148 | exe = createAuthenticodeHandler(args.exe); 2149 | if (exe == null) { console.log("Unable to parse executable file: " + args.exe); return; } 2150 | } 2151 | 2152 | // Parse the string resources and make any required changes 2153 | var resChanges = false, versionStrings = null; 2154 | if (exe != null) { 2155 | versionStrings = exe.getVersionInfo(); 2156 | var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion']; 2157 | for (var i in versionProperties) { 2158 | const prop = versionProperties[i], propl = prop.toLowerCase(); 2159 | if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; } 2160 | } 2161 | if (args['fileversionnumber'] != null) { 2162 | const fileVerSplit = args['fileversionnumber'].split('.'); 2163 | if (fileVerSplit.length != 4) { console.log("--fileversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; } 2164 | for (var i in fileVerSplit) { var n = parseInt(fileVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--fileversionnumber numbers must be between 0 and 65535."); return; } } 2165 | if (args['fileversionnumber'] != versionStrings['~FileVersion']) { versionStrings['~FileVersion'] = args['fileversionnumber']; resChanges = true; } 2166 | } 2167 | if (args['productversionnumber'] != null) { 2168 | const productVerSplit = args['productversionnumber'].split('.'); 2169 | if (productVerSplit.length != 4) { console.log("--productversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; } 2170 | for (var i in productVerSplit) { var n = parseInt(productVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--productversionnumber numbers must be between 0 and 65535."); return; } } 2171 | if (args['productversionnumber'] != versionStrings['~ProductVersion']) { versionStrings['~ProductVersion'] = args['productversionnumber']; resChanges = true; } 2172 | } 2173 | if (resChanges == true) { exe.setVersionInfo(versionStrings); } 2174 | } 2175 | 2176 | // Parse the icon changes 2177 | resChanges = false; 2178 | var icons = null, bitmaps = null; 2179 | if (exe != null) { 2180 | icons = exe.getIconInfo(); 2181 | bitmaps = exe.getBitmapInfo(); 2182 | if (typeof args['removeicongroup'] == 'string') { // If --removeicongroup is used, it's to remove an existing icon group 2183 | const groupsToRemove = args['removeicongroup'].split(','); 2184 | for (var i in groupsToRemove) { if (icons[groupsToRemove[i]] != null) { delete icons[groupsToRemove[i]]; resChanges = true; } } 2185 | } else if (typeof args['removeicongroup'] == 'number') { 2186 | if (icons[args['removeicongroup']] != null) { delete icons[args['removeicongroup']]; resChanges = true; } 2187 | } 2188 | if (typeof args['removebitmap'] == 'string') { // If --removebitmap is used 2189 | const bitmapsToRemove = args['removebitmap'].split(','); 2190 | for (var i in bitmapsToRemove) { if (bitmaps[bitmapsToRemove[i]] != null) { delete bitmaps[bitmapsToRemove[i]]; resChanges = true; } } 2191 | } else if (typeof args['removebitmap'] == 'number') { 2192 | if (bitmaps[args['removebitmap']] != null) { delete bitmaps[args['removebitmap']]; resChanges = true; } 2193 | } 2194 | if (typeof args['icon'] == 'string') { // If --icon is used, it's to add or replace an existing icon group 2195 | const iconToAddSplit = args['icon'].split(','); 2196 | if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; } 2197 | const iconName = parseInt(iconToAddSplit[0]); 2198 | const iconFile = iconToAddSplit[1]; 2199 | const icon = loadIcon(iconFile); 2200 | if (icon == null) { console.log("Unable to load icon: " + iconFile); return; } 2201 | if (icons[iconName] != null) { 2202 | const iconHash = hashObject(icon); // Compute the new icon group hash 2203 | const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash 2204 | if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group 2205 | } else { 2206 | icons[iconName] = icon; // We are adding an icon group 2207 | resChanges = true; 2208 | } 2209 | } 2210 | if (typeof args['bitmap'] == 'string') { // If --bitmap is used, it's to add or replace an existing bitmap 2211 | const bitmapToAddSplit = args['bitmap'].split(','); 2212 | if (bitmapToAddSplit.length != 2) { console.log("The --bitmap format is: --bitmap [number],[file]."); return; } 2213 | const bitmapName = parseInt(bitmapToAddSplit[0]); 2214 | const bitmapFile = bitmapToAddSplit[1]; 2215 | const bitmap = loadBitmap(bitmapFile); 2216 | if (bitmap == null) { console.log("Unable to load bitmap: " + bitmapFile); return; } 2217 | if (bitmaps[bitmapName] != null) { 2218 | const bitmapHash = hashObject(bitmap); // Compute the new bitmap hash 2219 | const bitmapHash2 = hashObject(bitmaps[bitmapName]); // Computer the old bitmap hash 2220 | if (bitmapHash != bitmapHash2) { bitmaps[bitmapName] = bitmap; resChanges = true; } // If different, replace the new bitmap 2221 | } else { 2222 | bitmaps[bitmapName] = bitmap; // We are adding an new bitmap 2223 | resChanges = true; 2224 | } 2225 | } 2226 | if (resChanges == true) { 2227 | exe.setIconInfo(icons); 2228 | exe.setBitmapInfo(bitmaps); 2229 | } 2230 | } 2231 | 2232 | // Execute the command 2233 | var command = process.argv[2].toLowerCase(); 2234 | if (command == 'info') { // Get signature information about an executable 2235 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2236 | if (args.json) { 2237 | var r = {}, stringInfo = exe.getVersionInfo(); 2238 | if (stringInfo != null) { 2239 | r.versionInfo = {}; 2240 | r.stringInfo = {}; 2241 | for (var i in stringInfo) { if (i.startsWith('~')) { r.versionInfo[i.substring(1)] = stringInfo[i]; } else { r.stringInfo[i] = stringInfo[i]; } } 2242 | } 2243 | if (exe.fileHashAlgo != null) { 2244 | r.signture = {}; 2245 | if (exe.fileHashAlgo != null) { r.signture.hashMethod = exe.fileHashAlgo; } 2246 | if (exe.fileHashSigned != null) { r.signture.hashSigned = exe.fileHashSigned.toString('hex'); } 2247 | if (exe.fileHashActual != null) { r.signture.hashActual = exe.fileHashActual.toString('hex'); } 2248 | if (exe.signingAttribs && exe.signingAttribs.length > 0) { r.signture.attributes = exe.signingAttribs; } 2249 | } 2250 | console.log(JSON.stringify(r, null, 2)); 2251 | } else { 2252 | var versionInfo = exe.getVersionInfo(); 2253 | if (versionInfo != null) { 2254 | console.log("Version Information:"); 2255 | for (var i in versionInfo) { if (i.startsWith('~') == true) { console.log(' ' + i.substring(1) + ': ' + versionInfo[i] + ''); } } 2256 | console.log("String Information:"); 2257 | for (var i in versionInfo) { if (i.startsWith('~') == false) { if (versionInfo[i] == null) { console.log(' ' + i + ': (Empty)'); } else { console.log(' ' + i + ': \"' + versionInfo[i] + '\"'); } } } 2258 | } 2259 | console.log("Checksum Information:"); 2260 | console.log(" Header CheckSum: 0x" + exe.header.peWindows.checkSum.toString(16)); 2261 | console.log(" Actual CheckSum: 0x" + exe.header.peWindows.checkSumActual.toString(16)); 2262 | console.log("Signature Information:"); 2263 | if (exe.fileHashAlgo != null) { 2264 | console.log(" Hash Method:", exe.fileHashAlgo); 2265 | if (exe.fileHashSigned != null) { console.log(" Signed Hash:", exe.fileHashSigned.toString('hex')); } 2266 | if (exe.fileHashActual != null) { console.log(" Actual Hash:", exe.fileHashActual.toString('hex')); } 2267 | } else { 2268 | console.log(" This file is not signed."); 2269 | } 2270 | if (exe.signingAttribs && exe.signingAttribs.length > 0) { console.log("Signature Attributes:"); for (var i in exe.signingAttribs) { console.log(' ' + exe.signingAttribs[i]); } } 2271 | } 2272 | } 2273 | if (command == 'header') { // Display the full executable header in JSON format 2274 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2275 | console.log(exe.header); 2276 | // Check that the header is valid 2277 | var ptr = 1024, sizeOfCode = 0, sizeOfInitializedData = 0; 2278 | for (var i in exe.header.sections) { 2279 | if (i == '.text') { sizeOfCode += exe.header.sections[i].rawSize; } else { sizeOfInitializedData += exe.header.sections[i].rawSize; } 2280 | if (exe.header.sections[i].rawAddr != ptr) { console.log('WARNING: ' + i + ' section should have a rawAddr or ' + ptr + ', but has ' + exe.header.sections[i].rawAddr + ' instead.'); } 2281 | ptr += exe.header.sections[i].rawSize; 2282 | } 2283 | if (exe.header.peStandard.sizeOfCode != sizeOfCode) { console.log('WARNING: Size of code is ' + exe.header.peStandard.sizeOfCode + ', should be ' + sizeOfCode + '.'); } 2284 | if (exe.header.peStandard.sizeOfInitializedData != sizeOfInitializedData) { console.log('WARNING: Size of initialized data is ' + exe.header.peStandard.sizeOfInitializedData + ', should be ' + sizeOfInitializedData + '.'); } 2285 | } 2286 | if (command == 'sections') { // Display sections in CSV format 2287 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2288 | var csvHeader = 'section'; 2289 | for (var i in exe.header.sections['.text']) { csvHeader += ',' + i; } 2290 | console.log(csvHeader); 2291 | for (var i in exe.header.sections) { 2292 | var csvData = i; 2293 | for (var j in exe.header.sections[i]) { csvData += ',' + exe.header.sections[i][j]; } 2294 | console.log(csvData); 2295 | } 2296 | } 2297 | if (command == 'sign') { // Sign an executable 2298 | if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; } 2299 | if (typeof args.hash == 'string') { args.hash = args.hash.toLowerCase(); if (['md5', 'sha224', 'sha256', 'sha384', 'sha512'].indexOf(args.hash) == -1) { console.log("Invalid hash method, must be SHA256 or SHA384"); return; } } 2300 | if (args.hash == null) { args.hash = 'sha384'; } 2301 | createOutFile(args, args.exe); 2302 | var cert = loadCertificates(args.pem); 2303 | if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); } 2304 | if (resChanges == false) { 2305 | console.log("Signing to " + args.out); 2306 | exe.sign(cert, args, function (err) { // Simple signing, copy most of the original file. 2307 | if (err == null) { console.log("Done."); } else { console.log(err); } 2308 | if (exe != null) { exe.close(); } 2309 | }); 2310 | return; 2311 | } else { 2312 | console.log("Changing resources and signing to " + args.out); 2313 | exe.writeExecutable(args, cert, function (err) { // Signing with resources decoded and re-encoded. 2314 | if (err == null) { console.log("Done."); } else { console.log(err); } 2315 | if (exe != null) { exe.close(); } 2316 | }); 2317 | return; 2318 | } 2319 | } 2320 | if (command == 'unsign') { // Unsign an executable 2321 | if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; } 2322 | createOutFile(args, args.exe); 2323 | if (resChanges == false) { 2324 | if (exe.header.signed) { 2325 | console.log("Unsigning to " + args.out); 2326 | exe.unsign(args); // Simple unsign, copy most of the original file. 2327 | console.log("Done."); 2328 | } else { 2329 | console.log("Executable is not signed."); 2330 | } 2331 | } else { 2332 | console.log("Changing resources and unsigning to " + args.out); 2333 | exe.writeExecutable(args, null, function (err) { // Unsigning with resources decoded and re-encoded. 2334 | if (err == null) { console.log("Done."); } else { console.log(err); } 2335 | if (exe != null) { exe.close(); } 2336 | }); 2337 | } 2338 | } 2339 | if (command == 'createcert') { // Create a code signing certificate and private key 2340 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } 2341 | if (typeof args.cn != 'string') { console.log("Missing --cn [name]"); return; } 2342 | if (typeof args.serial == 'string') { if (args.serial != parseInt(args.serial)) { console.log("Invalid serial number."); return; } else { args.serial = parseInt(args.serial); } } 2343 | if (typeof args.serial == 'number') { args.serial = '0' + args.serial; } // Serial number must be a integer string with a single leading '0' 2344 | const cert = createSelfSignedCert(args); 2345 | console.log("Writing to " + args.out); 2346 | fs.writeFileSync(args.out, pki.certificateToPem(cert.cert) + '\r\n' + pki.privateKeyToPem(cert.key)); 2347 | console.log("Done."); 2348 | } 2349 | if (command == 'bitmaps') { // Show bitmaps in the executable 2350 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2351 | if (args.json) { 2352 | var bitmapInfo = exe.getBitmapInfo(); 2353 | console.log(JSON.stringify(bitmapInfo, null, 2)); 2354 | } else { 2355 | var bitmapInfo = exe.getBitmapInfo(); 2356 | if (bitmapInfo != null) { 2357 | console.log("Bitmap Information:"); 2358 | for (var i in bitmapInfo) { console.log(' ' + i + ': ' + bitmapInfo[i].length + ' byte' + ((bitmapInfo[i].length > 1) ? 's' : '') + '.'); } 2359 | } 2360 | } 2361 | } 2362 | if (command == 'savebitmap') { // Save an bitmap to file 2363 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2364 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } 2365 | if (typeof args.bitmap != 'number') { console.log("Missing or incorrect --bitmap [number]"); return; } 2366 | const bitmapInfo = exe.getBitmapInfo(); 2367 | if (bitmapInfo[args.bitmap] == null) { console.log("Unknown bitmap: " + args.bitmap); return; } 2368 | 2369 | console.log("Writing to " + args.out); 2370 | var bitmapHeader = Buffer.from('424D000000000000000036000000', 'hex'); 2371 | bitmapHeader.writeUInt32LE(14 + bitmapInfo[args.bitmap].length, 2); // Write the full size of the bitmap file 2372 | fs.writeFileSync(args.out, Buffer.concat([bitmapHeader, bitmapInfo[args.bitmap]])); 2373 | console.log("Done."); 2374 | } 2375 | if (command == 'icons') { // Show icons in the executable 2376 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2377 | if (args.json) { 2378 | var r = {}, iconInfo = exe.getIconInfo(); 2379 | if (iconInfo != null) { r.iconInfo = iconInfo; } 2380 | console.log(JSON.stringify(r, null, 2)); 2381 | } else { 2382 | var iconInfo = exe.getIconInfo(); 2383 | if (iconInfo != null) { 2384 | console.log("Icon Information:"); 2385 | for (var i in iconInfo) { console.log(' Group ' + i + ':'); for (var j in iconInfo[i].icons) { console.log(' Icon ' + j + ': ' + ((iconInfo[i].icons[j].width == 0) ? 256 : iconInfo[i].icons[j].width) + 'x' + ((iconInfo[i].icons[j].height == 0) ? 256 : iconInfo[i].icons[j].height) + ', size: ' + iconInfo[i].icons[j].icon.length); } } 2386 | } 2387 | } 2388 | } 2389 | if (command == 'saveicon') { // Save an icon to file 2390 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2391 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } 2392 | if (typeof args.icon != 'number') { console.log("Missing or incorrect --icon [number]"); return; } 2393 | const iconInfo = exe.getIconInfo(); 2394 | var icon = null; 2395 | for (var i in iconInfo) { if (iconInfo[i].icons[args.icon]) { icon = iconInfo[i].icons[args.icon]; } } 2396 | if (icon == null) { console.log("Unknown icon: " + args.icon); return; } 2397 | 2398 | // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format) 2399 | var buf = Buffer.alloc(22); 2400 | buf.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor 2401 | buf.writeUInt16LE(1, 4); // Icon Count, always 1 in our case 2402 | buf[6] = icon.width; // Width (0 = 256) 2403 | buf[7] = icon.height; // Height (0 = 256) 2404 | buf[8] = icon.colorCount; // Colors 2405 | buf.writeUInt16LE(icon.planes, 10); // Color planes 2406 | buf.writeUInt16LE(icon.bitCount, 12); // Bits per pixel 2407 | buf.writeUInt32LE(icon.icon.length, 14); // Size 2408 | buf.writeUInt32LE(22, 18); // Offset, always 22 in our case 2409 | 2410 | console.log("Writing to " + args.out); 2411 | fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon])); 2412 | console.log("Done."); 2413 | } 2414 | if (command == 'saveicons') { // Save an icon group to file 2415 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2416 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } 2417 | if (typeof args.icongroup != 'number') { console.log("Missing or incorrect --icongroup [number]"); return; } 2418 | const iconInfo = exe.getIconInfo(); 2419 | const iconGroup = iconInfo[args.icongroup]; 2420 | if (iconGroup == null) { console.log("Invalid or incorrect --icongroup [number]"); return; } 2421 | 2422 | // Count the number of icons in the group 2423 | var iconCount = 0; 2424 | for (var i in iconGroup.icons) { iconCount++; } 2425 | 2426 | // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format) 2427 | const iconFileData = []; 2428 | const header = Buffer.alloc(6); 2429 | header.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor 2430 | header.writeUInt16LE(iconCount, 4); // Icon Count, always 1 in our case 2431 | iconFileData.push(header); 2432 | 2433 | // Store each icon header 2434 | var offsetPtr = 6 + (16 * iconCount); 2435 | for (var i in iconGroup.icons) { 2436 | const buf = Buffer.alloc(16); 2437 | buf[0] = iconGroup.icons[i].width; // Width (0 = 256) 2438 | buf[1] = iconGroup.icons[i].height; // Height (0 = 256) 2439 | buf[2] = iconGroup.icons[i].colorCount; // Colors 2440 | buf.writeUInt16LE(iconGroup.icons[i].planes, 4); // Color planes 2441 | buf.writeUInt16LE(iconGroup.icons[i].bitCount, 6); // Bits per pixel 2442 | buf.writeUInt32LE(iconGroup.icons[i].icon.length, 8); // Size 2443 | buf.writeUInt32LE(offsetPtr, 12); // Offset 2444 | offsetPtr += iconGroup.icons[i].icon.length; 2445 | iconFileData.push(buf); 2446 | } 2447 | 2448 | // Store each icon 2449 | for (var i in iconGroup.icons) { iconFileData.push(iconGroup.icons[i].icon); } 2450 | 2451 | // Write the .ico file 2452 | console.log("Writing to " + args.out); 2453 | fs.writeFileSync(args.out, Buffer.concat(iconFileData)); 2454 | console.log("Done."); 2455 | } 2456 | if (command == 'signblock') { // Display the raw signature block of the executable in hex 2457 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2458 | var buf = exe.getRawSignatureBlock(); 2459 | if (buf == null) { console.log("Executable is not signed."); return } else { console.log(buf.toString('hex')); return } 2460 | } 2461 | if (command == 'timestamp') { 2462 | if (exe == null) { console.log("Missing --exe [filename]"); return; } 2463 | if (exe.signature == null) { console.log("Executable is not signed."); return; } 2464 | if (typeof args.time != 'string') { console.log("Missing --time [url]"); return; } 2465 | createOutFile(args, args.exe); 2466 | console.log("Requesting time signature..."); 2467 | exe.timeStampRequest(args, function (err) { 2468 | if (err == null) { console.log("Done."); } else { console.log(err); } 2469 | if (exe != null) { exe.close(); } 2470 | }) 2471 | return; 2472 | } 2473 | 2474 | // Close the file 2475 | if (exe != null) { exe.close(); } 2476 | } 2477 | 2478 | // If this is the main module, run the command line version 2479 | if (require.main === module) { start(); } 2480 | 2481 | // Exports 2482 | module.exports.createAuthenticodeHandler = createAuthenticodeHandler; 2483 | module.exports.loadCertificates = loadCertificates; 2484 | module.exports.loadIcon = loadIcon; 2485 | module.exports.loadBitmap = loadBitmap; 2486 | module.exports.hashObject = hashObject; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authenticode-js", 3 | "version": "1.0.11", 4 | "keywords": [ 5 | "authenticode", 6 | "code signing" 7 | ], 8 | "homepage": "https://github.com/Ylianst/Authenticode-JS", 9 | "description": "Pure NodeJS code signing tool for Windows executables", 10 | "author": "Ylian Saint-Hilaire ", 11 | "main": "authenticode.js", 12 | "license": "Apache-2.0", 13 | "files": [ 14 | "*.js", 15 | "readme.txt", 16 | "license.txt" 17 | ], 18 | "dependencies": { 19 | "minimist": "^1.2.5", 20 | "node-forge": "^1.0.0" 21 | }, 22 | "engines": { 23 | "node": ">=10.0.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/Ylianst/Authenticode-JS" 28 | }, 29 | "readme": "readme.md" 30 | } 31 | -------------------------------------------------------------------------------- /pkcs7-modified.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript implementation of PKCS#7 v1.5. 3 | * 4 | * @author Stefan Siegl 5 | * @author Dave Longley 6 | * 7 | * Copyright (c) 2012 Stefan Siegl 8 | * Copyright (c) 2012-2015 Digital Bazaar, Inc. 9 | * 10 | * Currently this implementation only supports ContentType of EnvelopedData, 11 | * EncryptedData, or SignedData at the root level. The top level elements may 12 | * contain only a ContentInfo of ContentType Data, i.e. plain data. Further 13 | * nesting is not (yet) supported. 14 | * 15 | * The Forge validators for PKCS #7's ASN.1 structures are available from 16 | * a separate file pkcs7asn1.js, since those are referenced from other 17 | * PKCS standards like PKCS #12. 18 | */ 19 | var forge; 20 | try { 21 | forge = require('../node-forge/lib/forge'); 22 | require('../node-forge/lib/aes'); 23 | require('../node-forge/lib/asn1'); 24 | require('../node-forge/lib/des'); 25 | require('../node-forge/lib/oids'); 26 | require('../node-forge/lib/pem'); 27 | require('../node-forge/lib/pkcs7asn1'); 28 | require('../node-forge/lib/random'); 29 | require('../node-forge/lib/util'); 30 | require('../node-forge/lib/x509'); f 31 | } catch (ex) { } 32 | 33 | if (forge == null) { 34 | forge = require('./node_modules/node-forge/lib/forge'); 35 | require('./node_modules/node-forge/lib/aes'); 36 | require('./node_modules/node-forge/lib/asn1'); 37 | require('./node_modules/node-forge/lib/des'); 38 | require('./node_modules/node-forge/lib/oids'); 39 | require('./node_modules/node-forge/lib/pem'); 40 | require('./node_modules/node-forge/lib/pkcs7asn1'); 41 | require('./node_modules/node-forge/lib/random'); 42 | require('./node_modules/node-forge/lib/util'); 43 | require('./node_modules/node-forge/lib/x509'); 44 | } 45 | 46 | // shortcut for ASN.1 API 47 | var asn1 = forge.asn1; 48 | 49 | // shortcut for PKCS#7 API 50 | var p7 = module.exports = forge.pkcs7 = forge.pkcs7 || {}; 51 | 52 | /** 53 | * Converts a PKCS#7 message from PEM format. 54 | * 55 | * @param pem the PEM-formatted PKCS#7 message. 56 | * 57 | * @return the PKCS#7 message. 58 | */ 59 | p7.messageFromPem = function(pem) { 60 | var msg = forge.pem.decode(pem)[0]; 61 | 62 | if(msg.type !== 'PKCS7') { 63 | var error = new Error('Could not convert PKCS#7 message from PEM; PEM ' + 64 | 'header type is not "PKCS#7".'); 65 | error.headerType = msg.type; 66 | throw error; 67 | } 68 | if(msg.procType && msg.procType.type === 'ENCRYPTED') { 69 | throw new Error('Could not convert PKCS#7 message from PEM; PEM is encrypted.'); 70 | } 71 | 72 | // convert DER to ASN.1 object 73 | var obj = asn1.fromDer(msg.body); 74 | 75 | return p7.messageFromAsn1(obj); 76 | }; 77 | 78 | /** 79 | * Converts a PKCS#7 message to PEM format. 80 | * 81 | * @param msg The PKCS#7 message object 82 | * @param maxline The maximum characters per line, defaults to 64. 83 | * 84 | * @return The PEM-formatted PKCS#7 message. 85 | */ 86 | p7.messageToPem = function(msg, maxline) { 87 | // convert to ASN.1, then DER, then PEM-encode 88 | var pemObj = { 89 | type: 'PKCS7', 90 | body: asn1.toDer(msg.toAsn1()).getBytes() 91 | }; 92 | return forge.pem.encode(pemObj, {maxline: maxline}); 93 | }; 94 | 95 | /** 96 | * Converts a PKCS#7 message from an ASN.1 object. 97 | * 98 | * @param obj the ASN.1 representation of a ContentInfo. 99 | * 100 | * @return the PKCS#7 message. 101 | */ 102 | p7.messageFromAsn1 = function(obj) { 103 | // validate root level ContentInfo and capture data 104 | var capture = {}; 105 | var errors = []; 106 | if(!asn1.validate(obj, p7.asn1.contentInfoValidator, capture, errors)) { 107 | var error = new Error('Cannot read PKCS#7 message. ' + 108 | 'ASN.1 object is not an PKCS#7 ContentInfo.'); 109 | error.errors = errors; 110 | throw error; 111 | } 112 | 113 | var contentType = asn1.derToOid(capture.contentType); 114 | var msg; 115 | 116 | switch(contentType) { 117 | case forge.pki.oids.envelopedData: 118 | msg = p7.createEnvelopedData(); 119 | break; 120 | 121 | case forge.pki.oids.encryptedData: 122 | msg = p7.createEncryptedData(); 123 | break; 124 | 125 | case forge.pki.oids.signedData: 126 | msg = p7.createSignedData(); 127 | break; 128 | 129 | default: 130 | throw new Error('Cannot read PKCS#7 message. ContentType with OID ' + 131 | contentType + ' is not (yet) supported.'); 132 | } 133 | 134 | msg.fromAsn1(capture.content.value[0]); 135 | return msg; 136 | }; 137 | 138 | p7.createSignedData = function() { 139 | var msg = null; 140 | msg = { 141 | type: forge.pki.oids.signedData, 142 | version: 1, 143 | certificates: [], 144 | crls: [], 145 | // TODO: add json-formatted signer stuff here? 146 | signers: [], 147 | // populated during sign() 148 | digestAlgorithmIdentifiers: [], 149 | contentInfo: null, 150 | signerInfos: [], 151 | 152 | fromAsn1: function(obj) { 153 | // validate SignedData content block and capture data. 154 | _fromAsn1(msg, obj, p7.asn1.signedDataValidator); 155 | msg.certificates = []; 156 | msg.crls = []; 157 | msg.digestAlgorithmIdentifiers = []; 158 | msg.contentInfo = null; 159 | msg.signerInfos = []; 160 | 161 | if(msg.rawCapture.certificates) { 162 | var certs = msg.rawCapture.certificates.value; 163 | for(var i = 0; i < certs.length; ++i) { 164 | msg.certificates.push(forge.pki.certificateFromAsn1(certs[i])); 165 | } 166 | } 167 | 168 | // TODO: parse crls 169 | }, 170 | 171 | toAsn1: function() { 172 | // degenerate case with no content 173 | if(!msg.contentInfo) { 174 | msg.sign(); 175 | } 176 | 177 | var certs = []; 178 | for(var i = 0; i < msg.certificates.length; ++i) { 179 | certs.push(forge.pki.certificateToAsn1(msg.certificates[i])); 180 | } 181 | 182 | var crls = []; 183 | // TODO: implement CRLs 184 | 185 | // [0] SignedData 186 | var signedData = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 187 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 188 | // Version 189 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 190 | asn1.integerToDer(msg.version).getBytes()), 191 | // DigestAlgorithmIdentifiers 192 | asn1.create( 193 | asn1.Class.UNIVERSAL, asn1.Type.SET, true, 194 | msg.digestAlgorithmIdentifiers), 195 | // ContentInfo 196 | msg.contentInfo 197 | ]) 198 | ]); 199 | if(certs.length > 0) { 200 | // [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL 201 | signedData.value[0].value.push( 202 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, certs)); 203 | } 204 | if(crls.length > 0) { 205 | // [1] IMPLICIT CertificateRevocationLists OPTIONAL 206 | signedData.value[0].value.push( 207 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, crls)); 208 | } 209 | // SignerInfos 210 | signedData.value[0].value.push( 211 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, 212 | msg.signerInfos)); 213 | 214 | // ContentInfo 215 | return asn1.create( 216 | asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 217 | // ContentType 218 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 219 | asn1.oidToDer(msg.type).getBytes()), 220 | // [0] SignedData 221 | signedData 222 | ]); 223 | }, 224 | 225 | /** 226 | * Add (another) entity to list of signers. 227 | * 228 | * Note: If authenticatedAttributes are provided, then, per RFC 2315, 229 | * they must include at least two attributes: content type and 230 | * message digest. The message digest attribute value will be 231 | * auto-calculated during signing and will be ignored if provided. 232 | * 233 | * Here's an example of providing these two attributes: 234 | * 235 | * forge.pkcs7.createSignedData(); 236 | * p7.addSigner({ 237 | * issuer: cert.issuer.attributes, 238 | * serialNumber: cert.serialNumber, 239 | * key: privateKey, 240 | * digestAlgorithm: forge.pki.oids.sha1, 241 | * authenticatedAttributes: [{ 242 | * type: forge.pki.oids.contentType, 243 | * value: forge.pki.oids.data 244 | * }, { 245 | * type: forge.pki.oids.messageDigest 246 | * }] 247 | * }); 248 | * 249 | * TODO: Support [subjectKeyIdentifier] as signer's ID. 250 | * 251 | * @param signer the signer information: 252 | * key the signer's private key. 253 | * [certificate] a certificate containing the public key 254 | * associated with the signer's private key; use this option as 255 | * an alternative to specifying signer.issuer and 256 | * signer.serialNumber. 257 | * [issuer] the issuer attributes (eg: cert.issuer.attributes). 258 | * [serialNumber] the signer's certificate's serial number in 259 | * hexadecimal (eg: cert.serialNumber). 260 | * [digestAlgorithm] the message digest OID, as a string, to use 261 | * (eg: forge.pki.oids.sha1). 262 | * [authenticatedAttributes] an optional array of attributes 263 | * to also sign along with the content. 264 | */ 265 | addSigner: function(signer) { 266 | var issuer = signer.issuer; 267 | var serialNumber = signer.serialNumber; 268 | if(signer.certificate) { 269 | var cert = signer.certificate; 270 | if(typeof cert === 'string') { 271 | cert = forge.pki.certificateFromPem(cert); 272 | } 273 | issuer = cert.issuer.attributes; 274 | serialNumber = cert.serialNumber; 275 | } 276 | var key = signer.key; 277 | if(!key) { 278 | throw new Error( 279 | 'Could not add PKCS#7 signer; no private key specified.'); 280 | } 281 | if(typeof key === 'string') { 282 | key = forge.pki.privateKeyFromPem(key); 283 | } 284 | 285 | // ensure OID known for digest algorithm 286 | var digestAlgorithm = signer.digestAlgorithm || forge.pki.oids.sha1; 287 | switch(digestAlgorithm) { 288 | case forge.pki.oids.sha1: 289 | case forge.pki.oids.sha256: 290 | case forge.pki.oids.sha384: 291 | case forge.pki.oids.sha512: 292 | case forge.pki.oids.md5: 293 | break; 294 | default: 295 | throw new Error( 296 | 'Could not add PKCS#7 signer; unknown message digest algorithm: ' + 297 | digestAlgorithm); 298 | } 299 | 300 | // if authenticatedAttributes is present, then the attributes 301 | // must contain at least PKCS #9 content-type and message-digest 302 | var authenticatedAttributes = signer.authenticatedAttributes || []; 303 | if(authenticatedAttributes.length > 0) { 304 | var contentType = false; 305 | var messageDigest = false; 306 | for(var i = 0; i < authenticatedAttributes.length; ++i) { 307 | var attr = authenticatedAttributes[i]; 308 | if(!contentType && attr.type === forge.pki.oids.contentType) { 309 | contentType = true; 310 | if(messageDigest) { 311 | break; 312 | } 313 | continue; 314 | } 315 | if(!messageDigest && attr.type === forge.pki.oids.messageDigest) { 316 | messageDigest = true; 317 | if(contentType) { 318 | break; 319 | } 320 | continue; 321 | } 322 | } 323 | 324 | if(!contentType || !messageDigest) { 325 | throw new Error('Invalid signer.authenticatedAttributes. If ' + 326 | 'signer.authenticatedAttributes is specified, then it must ' + 327 | 'contain at least two attributes, PKCS #9 content-type and ' + 328 | 'PKCS #9 message-digest.'); 329 | } 330 | } 331 | 332 | msg.signers.push({ 333 | key: key, 334 | version: 1, 335 | issuer: issuer, 336 | serialNumber: serialNumber, 337 | digestAlgorithm: digestAlgorithm, 338 | signatureAlgorithm: forge.pki.oids.rsaEncryption, 339 | signature: null, 340 | authenticatedAttributes: authenticatedAttributes, 341 | unauthenticatedAttributes: [] 342 | }); 343 | }, 344 | 345 | /** 346 | * Signs the content. 347 | * @param options Options to apply when signing: 348 | * [detached] boolean. If signing should be done in detached mode. Defaults to false. 349 | */ 350 | sign: function(options) { 351 | options = options || {}; 352 | // auto-generate content info 353 | if(typeof msg.content !== 'object' || msg.contentInfo === null) { 354 | // use Data ContentInfo 355 | msg.contentInfo = asn1.create( 356 | asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 357 | // ContentType 358 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 359 | asn1.oidToDer(forge.pki.oids.data).getBytes()) 360 | ]); 361 | 362 | // add actual content, if present 363 | if('content' in msg) { 364 | var content; 365 | if(msg.content instanceof forge.util.ByteBuffer) { 366 | content = msg.content.bytes(); 367 | } else if(typeof msg.content === 'string') { 368 | content = forge.util.encodeUtf8(msg.content); 369 | } 370 | 371 | if (options.detached) { 372 | msg.detachedContent = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, content); 373 | } else { 374 | msg.contentInfo.value.push( 375 | // [0] EXPLICIT content 376 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 377 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, 378 | content) 379 | ])); 380 | } 381 | } 382 | } 383 | 384 | // no signers, return early (degenerate case for certificate container) 385 | if(msg.signers.length === 0) { 386 | return; 387 | } 388 | 389 | // generate digest algorithm identifiers 390 | var mds = addDigestAlgorithmIds(); 391 | 392 | // generate signerInfos 393 | addSignerInfos(mds); 394 | }, 395 | 396 | verify: function() { 397 | throw new Error('PKCS#7 signature verification not yet implemented.'); 398 | }, 399 | 400 | /** 401 | * Add a certificate. 402 | * 403 | * @param cert the certificate to add. 404 | */ 405 | addCertificate: function(cert) { 406 | // convert from PEM 407 | if(typeof cert === 'string') { 408 | cert = forge.pki.certificateFromPem(cert); 409 | } 410 | msg.certificates.push(cert); 411 | }, 412 | 413 | /** 414 | * Add a certificate revokation list. 415 | * 416 | * @param crl the certificate revokation list to add. 417 | */ 418 | addCertificateRevokationList: function(crl) { 419 | throw new Error('PKCS#7 CRL support not yet implemented.'); 420 | } 421 | }; 422 | return msg; 423 | 424 | function addDigestAlgorithmIds() { 425 | var mds = {}; 426 | 427 | for(var i = 0; i < msg.signers.length; ++i) { 428 | var signer = msg.signers[i]; 429 | var oid = signer.digestAlgorithm; 430 | if(!(oid in mds)) { 431 | // content digest 432 | mds[oid] = forge.md[forge.pki.oids[oid]].create(); 433 | } 434 | if(signer.authenticatedAttributes.length === 0) { 435 | // no custom attributes to digest; use content message digest 436 | signer.md = mds[oid]; 437 | } else { 438 | // custom attributes to be digested; use own message digest 439 | // TODO: optimize to just copy message digest state if that 440 | // feature is ever supported with message digests 441 | signer.md = forge.md[forge.pki.oids[oid]].create(); 442 | } 443 | } 444 | 445 | // add unique digest algorithm identifiers 446 | msg.digestAlgorithmIdentifiers = []; 447 | for(var oid in mds) { 448 | msg.digestAlgorithmIdentifiers.push( 449 | // AlgorithmIdentifier 450 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 451 | // algorithm 452 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 453 | asn1.oidToDer(oid).getBytes()), 454 | // parameters (null) 455 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') 456 | ])); 457 | } 458 | 459 | return mds; 460 | } 461 | 462 | function addSignerInfos(mds) { 463 | var content; 464 | 465 | if (msg.detachedContent) { 466 | // Signature has been made in detached mode. 467 | content = msg.detachedContent; 468 | } else { 469 | // Note: ContentInfo is a SEQUENCE with 2 values, second value is 470 | // the content field and is optional for a ContentInfo but required here 471 | // since signers are present 472 | // get ContentInfo content 473 | content = msg.contentInfo.value[1]; 474 | // skip [0] EXPLICIT content wrapper 475 | content = content.value[0]; 476 | } 477 | 478 | if(!content) { 479 | throw new Error( 480 | 'Could not sign PKCS#7 message; there is no content to sign.'); 481 | } 482 | 483 | // get ContentInfo content type 484 | var contentType = asn1.derToOid(msg.contentInfo.value[0].value); 485 | 486 | // serialize content 487 | var bytes = asn1.toDer(content); 488 | 489 | // skip identifier and length per RFC 2315 9.3 490 | // skip identifier (1 byte) 491 | bytes.getByte(); 492 | // read and discard length bytes 493 | asn1.getBerValueLength(bytes); 494 | bytes = bytes.getBytes(); 495 | 496 | // digest content DER value bytes 497 | for(var oid in mds) { 498 | mds[oid].start().update(bytes); 499 | } 500 | 501 | // sign content 502 | var signingTime = new Date(); 503 | for(var i = 0; i < msg.signers.length; ++i) { 504 | var signer = msg.signers[i]; 505 | 506 | if(signer.authenticatedAttributes.length === 0) { 507 | // if ContentInfo content type is not "Data", then 508 | // authenticatedAttributes must be present per RFC 2315 509 | if(contentType !== forge.pki.oids.data) { 510 | throw new Error( 511 | 'Invalid signer; authenticatedAttributes must be present ' + 512 | 'when the ContentInfo content type is not PKCS#7 Data.'); 513 | } 514 | } else { 515 | // process authenticated attributes 516 | // [0] IMPLICIT 517 | signer.authenticatedAttributesAsn1 = asn1.create( 518 | asn1.Class.CONTEXT_SPECIFIC, 0, true, []); 519 | 520 | // per RFC 2315, attributes are to be digested using a SET container 521 | // not the above [0] IMPLICIT container 522 | var attrsAsn1 = asn1.create( 523 | asn1.Class.UNIVERSAL, asn1.Type.SET, true, []); 524 | 525 | for(var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) { 526 | var attr = signer.authenticatedAttributes[ai]; 527 | if(attr.type === forge.pki.oids.messageDigest) { 528 | // use content message digest as value 529 | attr.value = mds[signer.digestAlgorithm].digest(); 530 | } else if(attr.type === forge.pki.oids.signingTime) { 531 | // auto-populate signing time if not already set 532 | if(!attr.value) { 533 | attr.value = signingTime; 534 | } 535 | } 536 | 537 | // convert to ASN.1 and push onto Attributes SET (for signing) and 538 | // onto authenticatedAttributesAsn1 to complete SignedData ASN.1 539 | // TODO: optimize away duplication 540 | attrsAsn1.value.push(_attributeToAsn1(attr)); 541 | signer.authenticatedAttributesAsn1.value.push(_attributeToAsn1(attr)); 542 | } 543 | 544 | // DER-serialize and digest SET OF attributes only 545 | bytes = asn1.toDer(attrsAsn1).getBytes(); 546 | signer.md.start().update(bytes); 547 | } 548 | 549 | // sign digest 550 | signer.signature = signer.key.sign(signer.md, 'RSASSA-PKCS1-V1_5'); 551 | } 552 | 553 | // add signer info 554 | msg.signerInfos = _signersToAsn1(msg.signers); 555 | } 556 | }; 557 | 558 | /** 559 | * Creates an empty PKCS#7 message of type EncryptedData. 560 | * 561 | * @return the message. 562 | */ 563 | p7.createEncryptedData = function() { 564 | var msg = null; 565 | msg = { 566 | type: forge.pki.oids.encryptedData, 567 | version: 0, 568 | encryptedContent: { 569 | algorithm: forge.pki.oids['aes256-CBC'] 570 | }, 571 | 572 | /** 573 | * Reads an EncryptedData content block (in ASN.1 format) 574 | * 575 | * @param obj The ASN.1 representation of the EncryptedData content block 576 | */ 577 | fromAsn1: function(obj) { 578 | // Validate EncryptedData content block and capture data. 579 | _fromAsn1(msg, obj, p7.asn1.encryptedDataValidator); 580 | }, 581 | 582 | /** 583 | * Decrypt encrypted content 584 | * 585 | * @param key The (symmetric) key as a byte buffer 586 | */ 587 | decrypt: function(key) { 588 | if(key !== undefined) { 589 | msg.encryptedContent.key = key; 590 | } 591 | _decryptContent(msg); 592 | } 593 | }; 594 | return msg; 595 | }; 596 | 597 | /** 598 | * Creates an empty PKCS#7 message of type EnvelopedData. 599 | * 600 | * @return the message. 601 | */ 602 | p7.createEnvelopedData = function() { 603 | var msg = null; 604 | msg = { 605 | type: forge.pki.oids.envelopedData, 606 | version: 0, 607 | recipients: [], 608 | encryptedContent: { 609 | algorithm: forge.pki.oids['aes256-CBC'] 610 | }, 611 | 612 | /** 613 | * Reads an EnvelopedData content block (in ASN.1 format) 614 | * 615 | * @param obj the ASN.1 representation of the EnvelopedData content block. 616 | */ 617 | fromAsn1: function(obj) { 618 | // validate EnvelopedData content block and capture data 619 | var capture = _fromAsn1(msg, obj, p7.asn1.envelopedDataValidator); 620 | msg.recipients = _recipientsFromAsn1(capture.recipientInfos.value); 621 | }, 622 | 623 | toAsn1: function() { 624 | // ContentInfo 625 | return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 626 | // ContentType 627 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 628 | asn1.oidToDer(msg.type).getBytes()), 629 | // [0] EnvelopedData 630 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 631 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 632 | // Version 633 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 634 | asn1.integerToDer(msg.version).getBytes()), 635 | // RecipientInfos 636 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, 637 | _recipientsToAsn1(msg.recipients)), 638 | // EncryptedContentInfo 639 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, 640 | _encryptedContentToAsn1(msg.encryptedContent)) 641 | ]) 642 | ]) 643 | ]); 644 | }, 645 | 646 | /** 647 | * Find recipient by X.509 certificate's issuer. 648 | * 649 | * @param cert the certificate with the issuer to look for. 650 | * 651 | * @return the recipient object. 652 | */ 653 | findRecipient: function(cert) { 654 | var sAttr = cert.issuer.attributes; 655 | 656 | for(var i = 0; i < msg.recipients.length; ++i) { 657 | var r = msg.recipients[i]; 658 | var rAttr = r.issuer; 659 | 660 | if(r.serialNumber !== cert.serialNumber) { 661 | continue; 662 | } 663 | 664 | if(rAttr.length !== sAttr.length) { 665 | continue; 666 | } 667 | 668 | var match = true; 669 | for(var j = 0; j < sAttr.length; ++j) { 670 | if(rAttr[j].type !== sAttr[j].type || 671 | rAttr[j].value !== sAttr[j].value) { 672 | match = false; 673 | break; 674 | } 675 | } 676 | 677 | if(match) { 678 | return r; 679 | } 680 | } 681 | 682 | return null; 683 | }, 684 | 685 | /** 686 | * Decrypt enveloped content 687 | * 688 | * @param recipient The recipient object related to the private key 689 | * @param privKey The (RSA) private key object 690 | */ 691 | decrypt: function(recipient, privKey) { 692 | if(msg.encryptedContent.key === undefined && recipient !== undefined && 693 | privKey !== undefined) { 694 | switch(recipient.encryptedContent.algorithm) { 695 | case forge.pki.oids.rsaEncryption: 696 | case forge.pki.oids.desCBC: 697 | var key = privKey.decrypt(recipient.encryptedContent.content); 698 | msg.encryptedContent.key = forge.util.createBuffer(key); 699 | break; 700 | 701 | default: 702 | throw new Error('Unsupported asymmetric cipher, ' + 703 | 'OID ' + recipient.encryptedContent.algorithm); 704 | } 705 | } 706 | 707 | _decryptContent(msg); 708 | }, 709 | 710 | /** 711 | * Add (another) entity to list of recipients. 712 | * 713 | * @param cert The certificate of the entity to add. 714 | */ 715 | addRecipient: function(cert) { 716 | msg.recipients.push({ 717 | version: 0, 718 | issuer: cert.issuer.attributes, 719 | serialNumber: cert.serialNumber, 720 | encryptedContent: { 721 | // We simply assume rsaEncryption here, since forge.pki only 722 | // supports RSA so far. If the PKI module supports other 723 | // ciphers one day, we need to modify this one as well. 724 | algorithm: forge.pki.oids.rsaEncryption, 725 | key: cert.publicKey 726 | } 727 | }); 728 | }, 729 | 730 | /** 731 | * Encrypt enveloped content. 732 | * 733 | * This function supports two optional arguments, cipher and key, which 734 | * can be used to influence symmetric encryption. Unless cipher is 735 | * provided, the cipher specified in encryptedContent.algorithm is used 736 | * (defaults to AES-256-CBC). If no key is provided, encryptedContent.key 737 | * is (re-)used. If that one's not set, a random key will be generated 738 | * automatically. 739 | * 740 | * @param [key] The key to be used for symmetric encryption. 741 | * @param [cipher] The OID of the symmetric cipher to use. 742 | */ 743 | encrypt: function(key, cipher) { 744 | // Part 1: Symmetric encryption 745 | if(msg.encryptedContent.content === undefined) { 746 | cipher = cipher || msg.encryptedContent.algorithm; 747 | key = key || msg.encryptedContent.key; 748 | 749 | var keyLen, ivLen, ciphFn; 750 | switch(cipher) { 751 | case forge.pki.oids['aes128-CBC']: 752 | keyLen = 16; 753 | ivLen = 16; 754 | ciphFn = forge.aes.createEncryptionCipher; 755 | break; 756 | 757 | case forge.pki.oids['aes192-CBC']: 758 | keyLen = 24; 759 | ivLen = 16; 760 | ciphFn = forge.aes.createEncryptionCipher; 761 | break; 762 | 763 | case forge.pki.oids['aes256-CBC']: 764 | keyLen = 32; 765 | ivLen = 16; 766 | ciphFn = forge.aes.createEncryptionCipher; 767 | break; 768 | 769 | case forge.pki.oids['des-EDE3-CBC']: 770 | keyLen = 24; 771 | ivLen = 8; 772 | ciphFn = forge.des.createEncryptionCipher; 773 | break; 774 | 775 | default: 776 | throw new Error('Unsupported symmetric cipher, OID ' + cipher); 777 | } 778 | 779 | if(key === undefined) { 780 | key = forge.util.createBuffer(forge.random.getBytes(keyLen)); 781 | } else if(key.length() != keyLen) { 782 | throw new Error('Symmetric key has wrong length; ' + 783 | 'got ' + key.length() + ' bytes, expected ' + keyLen + '.'); 784 | } 785 | 786 | // Keep a copy of the key & IV in the object, so the caller can 787 | // use it for whatever reason. 788 | msg.encryptedContent.algorithm = cipher; 789 | msg.encryptedContent.key = key; 790 | msg.encryptedContent.parameter = forge.util.createBuffer( 791 | forge.random.getBytes(ivLen)); 792 | 793 | var ciph = ciphFn(key); 794 | ciph.start(msg.encryptedContent.parameter.copy()); 795 | ciph.update(msg.content); 796 | 797 | // The finish function does PKCS#7 padding by default, therefore 798 | // no action required by us. 799 | if(!ciph.finish()) { 800 | throw new Error('Symmetric encryption failed.'); 801 | } 802 | 803 | msg.encryptedContent.content = ciph.output; 804 | } 805 | 806 | // Part 2: asymmetric encryption for each recipient 807 | for(var i = 0; i < msg.recipients.length; ++i) { 808 | var recipient = msg.recipients[i]; 809 | 810 | // Nothing to do, encryption already done. 811 | if(recipient.encryptedContent.content !== undefined) { 812 | continue; 813 | } 814 | 815 | switch(recipient.encryptedContent.algorithm) { 816 | case forge.pki.oids.rsaEncryption: 817 | recipient.encryptedContent.content = 818 | recipient.encryptedContent.key.encrypt( 819 | msg.encryptedContent.key.data); 820 | break; 821 | 822 | default: 823 | throw new Error('Unsupported asymmetric cipher, OID ' + 824 | recipient.encryptedContent.algorithm); 825 | } 826 | } 827 | } 828 | }; 829 | return msg; 830 | }; 831 | 832 | /** 833 | * Converts a single recipient from an ASN.1 object. 834 | * 835 | * @param obj the ASN.1 RecipientInfo. 836 | * 837 | * @return the recipient object. 838 | */ 839 | function _recipientFromAsn1(obj) { 840 | // validate EnvelopedData content block and capture data 841 | var capture = {}; 842 | var errors = []; 843 | if(!asn1.validate(obj, p7.asn1.recipientInfoValidator, capture, errors)) { 844 | var error = new Error('Cannot read PKCS#7 RecipientInfo. ' + 845 | 'ASN.1 object is not an PKCS#7 RecipientInfo.'); 846 | error.errors = errors; 847 | throw error; 848 | } 849 | 850 | return { 851 | version: capture.version.charCodeAt(0), 852 | issuer: forge.pki.RDNAttributesAsArray(capture.issuer), 853 | serialNumber: forge.util.createBuffer(capture.serial).toHex(), 854 | encryptedContent: { 855 | algorithm: asn1.derToOid(capture.encAlgorithm), 856 | parameter: capture.encParameter ? capture.encParameter.value : undefined, 857 | content: capture.encKey 858 | } 859 | }; 860 | } 861 | 862 | /** 863 | * Converts a single recipient object to an ASN.1 object. 864 | * 865 | * @param obj the recipient object. 866 | * 867 | * @return the ASN.1 RecipientInfo. 868 | */ 869 | function _recipientToAsn1(obj) { 870 | return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 871 | // Version 872 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 873 | asn1.integerToDer(obj.version).getBytes()), 874 | // IssuerAndSerialNumber 875 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 876 | // Name 877 | forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), 878 | // Serial 879 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 880 | forge.util.hexToBytes(obj.serialNumber)) 881 | ]), 882 | // KeyEncryptionAlgorithmIdentifier 883 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 884 | // Algorithm 885 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 886 | asn1.oidToDer(obj.encryptedContent.algorithm).getBytes()), 887 | // Parameter, force NULL, only RSA supported for now. 888 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') 889 | ]), 890 | // EncryptedKey 891 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, 892 | obj.encryptedContent.content) 893 | ]); 894 | } 895 | 896 | /** 897 | * Map a set of RecipientInfo ASN.1 objects to recipient objects. 898 | * 899 | * @param infos an array of ASN.1 representations RecipientInfo (i.e. SET OF). 900 | * 901 | * @return an array of recipient objects. 902 | */ 903 | function _recipientsFromAsn1(infos) { 904 | var ret = []; 905 | for(var i = 0; i < infos.length; ++i) { 906 | ret.push(_recipientFromAsn1(infos[i])); 907 | } 908 | return ret; 909 | } 910 | 911 | /** 912 | * Map an array of recipient objects to ASN.1 RecipientInfo objects. 913 | * 914 | * @param recipients an array of recipientInfo objects. 915 | * 916 | * @return an array of ASN.1 RecipientInfos. 917 | */ 918 | function _recipientsToAsn1(recipients) { 919 | var ret = []; 920 | for(var i = 0; i < recipients.length; ++i) { 921 | ret.push(_recipientToAsn1(recipients[i])); 922 | } 923 | return ret; 924 | } 925 | 926 | /** 927 | * Converts a single signer from an ASN.1 object. 928 | * 929 | * @param obj the ASN.1 representation of a SignerInfo. 930 | * 931 | * @return the signer object. 932 | */ 933 | function _signerFromAsn1(obj) { 934 | // validate EnvelopedData content block and capture data 935 | var capture = {}; 936 | var errors = []; 937 | if(!asn1.validate(obj, p7.asn1.signerInfoValidator, capture, errors)) { 938 | var error = new Error('Cannot read PKCS#7 SignerInfo. ' + 939 | 'ASN.1 object is not an PKCS#7 SignerInfo.'); 940 | error.errors = errors; 941 | throw error; 942 | } 943 | 944 | var rval = { 945 | version: capture.version.charCodeAt(0), 946 | issuer: forge.pki.RDNAttributesAsArray(capture.issuer), 947 | serialNumber: forge.util.createBuffer(capture.serial).toHex(), 948 | digestAlgorithm: asn1.derToOid(capture.digestAlgorithm), 949 | signatureAlgorithm: asn1.derToOid(capture.signatureAlgorithm), 950 | signature: capture.signature, 951 | authenticatedAttributes: [], 952 | unauthenticatedAttributes: [] 953 | }; 954 | 955 | // TODO: convert attributes 956 | var authenticatedAttributes = capture.authenticatedAttributes || []; 957 | var unauthenticatedAttributes = capture.unauthenticatedAttributes || []; 958 | 959 | return rval; 960 | } 961 | 962 | /** 963 | * Converts a single signerInfo object to an ASN.1 object. 964 | * 965 | * @param obj the signerInfo object. 966 | * 967 | * @return the ASN.1 representation of a SignerInfo. 968 | */ 969 | function _signerToAsn1(obj) { 970 | // SignerInfo 971 | var rval = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 972 | // version 973 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 974 | asn1.integerToDer(obj.version).getBytes()), 975 | // issuerAndSerialNumber 976 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 977 | // name 978 | forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), 979 | // serial 980 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, 981 | forge.util.hexToBytes(obj.serialNumber)) 982 | ]), 983 | // digestAlgorithm 984 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 985 | // algorithm 986 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 987 | asn1.oidToDer(obj.digestAlgorithm).getBytes()), 988 | // parameters (null) 989 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') 990 | ]) 991 | ]); 992 | 993 | // authenticatedAttributes (OPTIONAL) 994 | if(obj.authenticatedAttributesAsn1) { 995 | // add ASN.1 previously generated during signing 996 | rval.value.push(obj.authenticatedAttributesAsn1); 997 | } 998 | 999 | // digestEncryptionAlgorithm 1000 | rval.value.push(asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1001 | // algorithm 1002 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 1003 | asn1.oidToDer(obj.signatureAlgorithm).getBytes()), 1004 | // parameters (null) 1005 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') 1006 | ])); 1007 | 1008 | // encryptedDigest 1009 | rval.value.push(asn1.create( 1010 | asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature)); 1011 | 1012 | // unauthenticatedAttributes (OPTIONAL) 1013 | if(obj.unauthenticatedAttributes.length > 0) { 1014 | // [1] IMPLICIT 1015 | var attrsAsn1 = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, []); 1016 | for(var i = 0; i < obj.unauthenticatedAttributes.length; ++i) { 1017 | var attr = obj.unauthenticatedAttributes[i]; 1018 | attrsAsn1.values.push(_attributeToAsn1(attr)); 1019 | } 1020 | rval.value.push(attrsAsn1); 1021 | } 1022 | 1023 | return rval; 1024 | } 1025 | 1026 | /** 1027 | * Map a set of SignerInfo ASN.1 objects to an array of signer objects. 1028 | * 1029 | * @param signerInfoAsn1s an array of ASN.1 SignerInfos (i.e. SET OF). 1030 | * 1031 | * @return an array of signers objects. 1032 | */ 1033 | function _signersFromAsn1(signerInfoAsn1s) { 1034 | var ret = []; 1035 | for(var i = 0; i < signerInfoAsn1s.length; ++i) { 1036 | ret.push(_signerFromAsn1(signerInfoAsn1s[i])); 1037 | } 1038 | return ret; 1039 | } 1040 | 1041 | /** 1042 | * Map an array of signer objects to ASN.1 objects. 1043 | * 1044 | * @param signers an array of signer objects. 1045 | * 1046 | * @return an array of ASN.1 SignerInfos. 1047 | */ 1048 | function _signersToAsn1(signers) { 1049 | var ret = []; 1050 | for(var i = 0; i < signers.length; ++i) { 1051 | ret.push(_signerToAsn1(signers[i])); 1052 | } 1053 | return ret; 1054 | } 1055 | 1056 | /** 1057 | * Convert an attribute object to an ASN.1 Attribute. 1058 | * 1059 | * @param attr the attribute object. 1060 | * 1061 | * @return the ASN.1 Attribute. 1062 | */ 1063 | function _attributeToAsn1(attr) { 1064 | var value; 1065 | 1066 | // TODO: generalize to support more attributes 1067 | if(attr.type === forge.pki.oids.contentType) { 1068 | value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 1069 | asn1.oidToDer(attr.value).getBytes()); 1070 | } else if(attr.type === forge.pki.oids.messageDigest) { 1071 | value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, 1072 | attr.value.bytes()); 1073 | } else if(attr.type === forge.pki.oids.signingTime) { 1074 | /* Note per RFC 2985: Dates between 1 January 1950 and 31 December 2049 1075 | (inclusive) MUST be encoded as UTCTime. Any dates with year values 1076 | before 1950 or after 2049 MUST be encoded as GeneralizedTime. [Further,] 1077 | UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST 1078 | include seconds (i.e., times are YYMMDDHHMMSSZ), even where the 1079 | number of seconds is zero. Midnight (GMT) must be represented as 1080 | "YYMMDD000000Z". */ 1081 | // TODO: make these module-level constants 1082 | var jan_1_1950 = new Date('1950-01-01T00:00:00Z'); 1083 | var jan_1_2050 = new Date('2050-01-01T00:00:00Z'); 1084 | var date = attr.value; 1085 | if(typeof date === 'string') { 1086 | // try to parse date 1087 | var timestamp = Date.parse(date); 1088 | if(!isNaN(timestamp)) { 1089 | date = new Date(timestamp); 1090 | } else if(date.length === 13) { 1091 | // YYMMDDHHMMSSZ (13 chars for UTCTime) 1092 | date = asn1.utcTimeToDate(date); 1093 | } else { 1094 | // assume generalized time 1095 | date = asn1.generalizedTimeToDate(date); 1096 | } 1097 | } 1098 | 1099 | if(date >= jan_1_1950 && date < jan_1_2050) { 1100 | value = asn1.create( 1101 | asn1.Class.UNIVERSAL, asn1.Type.UTCTIME, false, 1102 | asn1.dateToUtcTime(date)); 1103 | } else { 1104 | value = asn1.create( 1105 | asn1.Class.UNIVERSAL, asn1.Type.GENERALIZEDTIME, false, 1106 | asn1.dateToGeneralizedTime(date)); 1107 | } 1108 | } 1109 | 1110 | // Added this line to support custom attributes 1111 | if ((value == null) && (attr != null) && (attr.value != null)) { value = attr.value; } 1112 | 1113 | // TODO: expose as common API call 1114 | // create a RelativeDistinguishedName set 1115 | // each value in the set is an AttributeTypeAndValue first 1116 | // containing the type (an OID) and second the value 1117 | return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1118 | // AttributeType 1119 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 1120 | asn1.oidToDer(attr.type).getBytes()), 1121 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ 1122 | // AttributeValue 1123 | value 1124 | ]) 1125 | ]); 1126 | } 1127 | 1128 | /** 1129 | * Map messages encrypted content to ASN.1 objects. 1130 | * 1131 | * @param ec The encryptedContent object of the message. 1132 | * 1133 | * @return ASN.1 representation of the encryptedContent object (SEQUENCE). 1134 | */ 1135 | function _encryptedContentToAsn1(ec) { 1136 | return [ 1137 | // ContentType, always Data for the moment 1138 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 1139 | asn1.oidToDer(forge.pki.oids.data).getBytes()), 1140 | // ContentEncryptionAlgorithmIdentifier 1141 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ 1142 | // Algorithm 1143 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, 1144 | asn1.oidToDer(ec.algorithm).getBytes()), 1145 | // Parameters (IV) 1146 | !ec.parameter ? 1147 | undefined : 1148 | asn1.create( 1149 | asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, 1150 | ec.parameter.getBytes()) 1151 | ]), 1152 | // [0] EncryptedContent 1153 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ 1154 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, 1155 | ec.content.getBytes()) 1156 | ]) 1157 | ]; 1158 | } 1159 | 1160 | /** 1161 | * Reads the "common part" of an PKCS#7 content block (in ASN.1 format) 1162 | * 1163 | * This function reads the "common part" of the PKCS#7 content blocks 1164 | * EncryptedData and EnvelopedData, i.e. version number and symmetrically 1165 | * encrypted content block. 1166 | * 1167 | * The result of the ASN.1 validate and capture process is returned 1168 | * to allow the caller to extract further data, e.g. the list of recipients 1169 | * in case of a EnvelopedData object. 1170 | * 1171 | * @param msg the PKCS#7 object to read the data to. 1172 | * @param obj the ASN.1 representation of the content block. 1173 | * @param validator the ASN.1 structure validator object to use. 1174 | * 1175 | * @return the value map captured by validator object. 1176 | */ 1177 | function _fromAsn1(msg, obj, validator) { 1178 | var capture = {}; 1179 | var errors = []; 1180 | if(!asn1.validate(obj, validator, capture, errors)) { 1181 | var error = new Error('Cannot read PKCS#7 message. ' + 1182 | 'ASN.1 object is not a supported PKCS#7 message.'); 1183 | error.errors = error; 1184 | throw error; 1185 | } 1186 | 1187 | // Check contentType, so far we only support (raw) Data. 1188 | var contentType = asn1.derToOid(capture.contentType); 1189 | if(contentType !== forge.pki.oids.data) { 1190 | throw new Error('Unsupported PKCS#7 message. ' + 1191 | 'Only wrapped ContentType Data supported.'); 1192 | } 1193 | 1194 | if(capture.encryptedContent) { 1195 | var content = ''; 1196 | if(forge.util.isArray(capture.encryptedContent)) { 1197 | for(var i = 0; i < capture.encryptedContent.length; ++i) { 1198 | if(capture.encryptedContent[i].type !== asn1.Type.OCTETSTRING) { 1199 | throw new Error('Malformed PKCS#7 message, expecting encrypted ' + 1200 | 'content constructed of only OCTET STRING objects.'); 1201 | } 1202 | content += capture.encryptedContent[i].value; 1203 | } 1204 | } else { 1205 | content = capture.encryptedContent; 1206 | } 1207 | msg.encryptedContent = { 1208 | algorithm: asn1.derToOid(capture.encAlgorithm), 1209 | parameter: forge.util.createBuffer(capture.encParameter.value), 1210 | content: forge.util.createBuffer(content) 1211 | }; 1212 | } 1213 | 1214 | if(capture.content) { 1215 | var content = ''; 1216 | if(forge.util.isArray(capture.content)) { 1217 | for(var i = 0; i < capture.content.length; ++i) { 1218 | if(capture.content[i].type !== asn1.Type.OCTETSTRING) { 1219 | throw new Error('Malformed PKCS#7 message, expecting ' + 1220 | 'content constructed of only OCTET STRING objects.'); 1221 | } 1222 | content += capture.content[i].value; 1223 | } 1224 | } else { 1225 | content = capture.content; 1226 | } 1227 | msg.content = forge.util.createBuffer(content); 1228 | } 1229 | 1230 | msg.version = capture.version.charCodeAt(0); 1231 | msg.rawCapture = capture; 1232 | 1233 | return capture; 1234 | } 1235 | 1236 | /** 1237 | * Decrypt the symmetrically encrypted content block of the PKCS#7 message. 1238 | * 1239 | * Decryption is skipped in case the PKCS#7 message object already has a 1240 | * (decrypted) content attribute. The algorithm, key and cipher parameters 1241 | * (probably the iv) are taken from the encryptedContent attribute of the 1242 | * message object. 1243 | * 1244 | * @param The PKCS#7 message object. 1245 | */ 1246 | function _decryptContent(msg) { 1247 | if(msg.encryptedContent.key === undefined) { 1248 | throw new Error('Symmetric key not available.'); 1249 | } 1250 | 1251 | if(msg.content === undefined) { 1252 | var ciph; 1253 | 1254 | switch(msg.encryptedContent.algorithm) { 1255 | case forge.pki.oids['aes128-CBC']: 1256 | case forge.pki.oids['aes192-CBC']: 1257 | case forge.pki.oids['aes256-CBC']: 1258 | ciph = forge.aes.createDecryptionCipher(msg.encryptedContent.key); 1259 | break; 1260 | 1261 | case forge.pki.oids['desCBC']: 1262 | case forge.pki.oids['des-EDE3-CBC']: 1263 | ciph = forge.des.createDecryptionCipher(msg.encryptedContent.key); 1264 | break; 1265 | 1266 | default: 1267 | throw new Error('Unsupported symmetric cipher, OID ' + 1268 | msg.encryptedContent.algorithm); 1269 | } 1270 | ciph.start(msg.encryptedContent.parameter); 1271 | ciph.update(msg.encryptedContent.content); 1272 | 1273 | if(!ciph.finish()) { 1274 | throw new Error('Symmetric decryption failed.'); 1275 | } 1276 | 1277 | msg.content = ciph.output; 1278 | } 1279 | } 1280 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Authenticode-JS 2 | 3 | Authenticode-JS is a fully NodeJS tool that can sign, unsign, timestamp and change version and icon resources in a Windows executables. You can also get information about an executable signature and generate your own code signing certificate. 4 | 5 | This module can be useful when wanting to sign windows executable on non-windows platforms, like signing a Windows executable on Linux or macOS. This code is used by [MeshCentral](https://meshcentral.com) to code-sign the agent on each installed server. 6 | 7 | ## Using as a command line tool 8 | 9 | You can run authenticode.js as a command line tool like this: 10 | 11 | ``` 12 | node authenticode 13 | ``` 14 | 15 | You will see this: 16 | 17 | ``` 18 | MeshCentral Authenticode Tool. 19 | Usage: 20 | node authenticode.js [command] [options] 21 | Commands: 22 | info: Show information about an executable. 23 | --exe [file] Required executable to view information. 24 | --json Show information in JSON format. 25 | sign: Sign an executable. 26 | --exe [file] Required executable to sign. 27 | --out [file] Resulting signed executable. 28 | --pem [pemfile] Certificate & private key to sign the executable with. 29 | --desc [description] Description string to embbed into signature. 30 | --url [url] URL to embbed into signature. 31 | --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512. 32 | --time [url] The time signing server URL. 33 | --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http:// 34 | unsign: Remove the signature from the executable. 35 | --exe [file] Required executable to un-sign. 36 | --out [file] Resulting executable with signature removed. 37 | createcert: Create a code signging self-signed certificate and key. 38 | --out [pemfile] Required certificate file to create. 39 | --cn [value] Required certificate common name. 40 | --country [value] Certificate country name. 41 | --state [value] Certificate state name. 42 | --locality [value] Certificate locality name. 43 | --org [value] Certificate organization name. 44 | --ou [value] Certificate organization unit name. 45 | --serial [value] Certificate serial number. 46 | timestamp: Add a signed timestamp to an already signed executable. 47 | --exe [file] Required executable to timestamp. 48 | --out [file] Resulting signed executable. 49 | --time [url] The time signing server URL. 50 | --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http:// 51 | icons: Show the icon resources in the executable. 52 | --exe [file] Input executable. 53 | saveicon: Save a single icon bitmap to a .ico file. 54 | --exe [file] Input executable. 55 | --out [file] Resulting .ico file. 56 | --icon [number] Icon number to save to file. 57 | saveicons: Save an icon group to a .ico file. 58 | --exe [file] Input executable. 59 | --out [file] Resulting .ico file. 60 | --icongroup [groupNumber] Icon groupnumber to save to file. 61 | 62 | Note that certificate PEM files must first have the signing certificate, 63 | followed by all certificates that form the trust chain. 64 | 65 | When doing sign/unsign, you can also change resource properties of the generated file. 66 | 67 | --fileversionnumber n.n.n.n 68 | --productversionnumber n.n.n.n 69 | --filedescription [value] 70 | --fileversion [value] 71 | --internalname [value] 72 | --legalcopyright [value] 73 | --originalfilename [value] 74 | --productname [value] 75 | --productversion [value] 76 | --removeicongroup [number] 77 | --icon [groupNumber],[filename.ico] 78 | ``` 79 | 80 | ## Using as a module 81 | 82 | You can use authenticode-js in your code as an external module. To load an exectuable call `createAuthenticodeHandler`. This call will return null if the executable can't be loaded or is invalid. 83 | 84 | ``` 85 | var exehandler = require("authenticode").createAuthenticodeHandler("/tmp/windowsexecutable.exe"); 86 | ``` 87 | 88 | You can then get information about the existing signature of this executable: 89 | 90 | ``` 91 | console.log(exehandler.header); // Display executable header information. 92 | console.log(exehandler.fileHashAlgo); // Display the type of hashing used, typically 'sha256' or 'sha384'. 93 | console.log(exehandler.fileHashSigned); // Display the hash included in the signature. 94 | console.log(exehandler.fileHashActual); // Display the actual hash of the file. 95 | console.log(exehandler.signingAttribs); // DIsplay any signing attributes, typically description and/or url. 96 | ``` 97 | 98 | You can remove the signature of an executable like this: 99 | 100 | ``` 101 | exehandler.unsign({ out: '/tmp/windowsexecutable-unsigned.exe' }); 102 | ``` 103 | 104 | You can sign or re-sign an exectuable and timestamp it. You can load the certificate PEM file using the built in `loadcertificate()` method, or you can pass null as the certificate and a test certificate will be generated and used to sign the executable. The `desc` and `url` options are optional. 105 | 106 | ``` 107 | const cert = require("authenticode").loadCertificates("/tmp/signingcert.pem"); 108 | exehandler.sign(cert, { out: '/tmp/windowsexecutable-signed.exe', desc: "description", url: "https://sample.org", time: "http://timestamp.sectigo.com" }); 109 | ``` 110 | 111 | You can generate your own code signing certificate. All of the certificate creation values are optional, but `cn` should really be present. Oddly, the serial number should be an integer that starts with a single leading `0`, for example `0123` or `01111`. Once created, you can use node-forge to save the certificate. 112 | 113 | ``` 114 | const cert = require("authenticode").createSelfSignedCert({ cn: "commonName", serial: "012345", state: "state", locality: "locality", country: "x", org: "org", orgunit: "orgunitf" }); 115 | require('fs').writeFileSync("/tmp/signingcert.pem", require('node-forge').pki.certificateToPem(cert.cert) + '\r\n' + require('node-forge').pki.privateKeyToPem(cert.key)); 116 | ``` 117 | 118 | ## Certificate file format 119 | 120 | In the above examples, a `signingcert.pem` file contains the certificate and private key used to sign the windows executable. The format of this .pem file is as follows: 121 | 122 | ``` 123 | This is the code signing public certificate, must be first. 124 | -----BEGIN CERTIFICATE----- 125 | MIIEQDCC... 126 | -----END CERTIFICATE----- 127 | 128 | This is the code signing certificate private key, can be anywhere in the file. 129 | -----BEGIN RSA PRIVATE KEY----- 130 | MIIG5AIB... 131 | -----END RSA PRIVATE KEY----- 132 | 133 | This is one of the public certificate in the validation chain. 134 | -----BEGIN CERTIFICATE----- 135 | MIIEQzCCA... 136 | -----END CERTIFICATE----- 137 | 138 | This is another public certificate in the validation chain, you can put as many as needed. 139 | -----BEGIN CERTIFICATE----- 140 | MIIEQzCCA... 141 | -----END CERTIFICATE----- 142 | ``` 143 | 144 | ## License 145 | This software is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). --------------------------------------------------------------------------------