├── makecertfile ├── README.md └── gogcheck /makecertfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script creates a certificate file, which can be used by "gogcheck" 4 | # (https://github.com/hippie68/gogcheck) to verify GOG installers. 5 | # Be aware the script downloads files from external servers - Mozilla's 6 | # certificate list and additional certificates, specified below - and downloads 7 | # (if necessary) and executes the third party script "mk-ca-bundle.pl" from the 8 | # cURL GitHub repository. 9 | 10 | additional_certs=' 11 | https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem 12 | ' 13 | 14 | # Functions: ################################################################## 15 | 16 | # Asks the user the yes-no question $1, returning 1 on "yes" and 0 on "no" 17 | ask() { 18 | local reply 19 | read -rp "$1 (y/n) " reply 20 | if [[ $reply == 'y' ]]; then 21 | return 0 22 | else 23 | return 1 24 | fi 25 | } 26 | 27 | # Script starts below: ######################################################## 28 | 29 | script_name='mk-ca-bundle.pl' 30 | script_url='https://raw.githubusercontent.com/curl/curl/master/scripts/mk-ca-bundle.pl' 31 | script_sha256sums=( 32 | 4398394eb7b8e7879fadde3497bcc053a7cf19f53a5f2b79eac43ea2bb1e2192 # 2bc1d77 Jan 3, 2023 33 | 7128aba721fa9219acd3212308e4d1ecf2649b5bf1edd0160eff7117fce67325 # 4fab113 Jun 29, 2024 34 | afcd8348c4f9b2c4a1890da6890b3eeaa0d9e2b16fd07bea7340b2e8532b0964 # 32f9130 Aug 9, 2024 35 | b318bdfc84bb5fc71431da61f9663ce01739536bb1b393d46315257b3300e412 # 448df98 Nov 12, 2024 36 | e2e3dec7889eb791e2006a155ac24326a45ac8c12c9738a5bd5d03638b2fa1e8 # eeed87f Apr 8, 2025 37 | 1a9fbe1b4e3c2b6a6b8e23357d8a8907c2c93d67cb6b1b3c2d0f884c7f5a4e6e # 3fcddc8 Apr 29, 2025 38 | 0c66ca4c5418e2137d20e65bd10df0b193bf9578f7e7a947e5413e198652b4bd # 4f055fe May 12, 2025 39 | 4f9e1937ff451117dff633b03ceed63bb8550a0bca6c5c07e60c8d349b826a2d # ddcfd2d Jul 12, 2025 40 | ) 41 | script_options=(-fup 'CODE_SIGNING:TRUSTED,MUST_VERIFY_TRUST') 42 | 43 | if [[ ! $1 ]]; then 44 | echo "Usage: ${0##*/} OUTPUT_FILENAME" 45 | exit 0 46 | else 47 | output=$1 48 | fi 49 | 50 | # Check for required tools 51 | for tool in cURL Perl; do 52 | if ! hash ${tool,,} 2> /dev/null; then 53 | echo "Please install $tool to be able to use this script." 54 | exit 1 55 | fi 56 | done 57 | 58 | # Check for the Perl script mk-ca-bundle.pl and download it, if necessary 59 | if [[ -f $script_name ]]; then 60 | echo "Found required script \"$script_name\" in the current directory." 61 | elif hash "$script_name" 2> /dev/null; then 62 | echo "Required script \"$script_name\" is available on this system." 63 | else 64 | echo "Required script \"$script_name\" not found. Downloading now..." 65 | if ! curl --silent "$script_url" > "$script_name"; then 66 | echo "Could not download the required script \"$script_name\"." 67 | echo 'Please download it manually or try again.' 68 | echo "The URL is \"$script_url\".". 69 | exit 1 70 | fi 71 | echo '...done.' 72 | echo 'Note: The downloaded script is located in the current directory, filename' 73 | echo " \"$script_name\". Remove it manually later if you don't need it anymore." 74 | fi 75 | 76 | # Download Mozilla's certificate file and extract required certs to PEM format 77 | echo "Now executing the script \"$script_name\":" 78 | if hash "$script_name" 2> /dev/null; then 79 | "$script_name" "${script_options[@]}" "$output" || exit $? 80 | else 81 | if ! hash sha256sum 2> /dev/null; then 82 | ask 'Program "sha256sum" not found. The script'\''s checksum cannot be verified.\nContinue anyway?' || exit 1 83 | else 84 | checksum=$(sha256sum "$script_name") 85 | if [[ ${script_sha256sums[*]} != *${checksum%% *}* ]]; then 86 | ask 'The script has an unexpected checksum. Execute it anyway?' || exit 1 87 | fi 88 | fi 89 | perl "$script_name" "${script_options[@]}" "$output" || exit $? 90 | fi 91 | if [[ ! -f $output ]]; then 92 | echo "Could not find file \"$output\"." 93 | exit 1 94 | fi 95 | echo "Script \"$script_name\" finished." 96 | 97 | # Add missing certificates 98 | echo 'Adding missing certificates...' 99 | for cert in $additional_certs; do 100 | echo " - $cert" 101 | { 102 | printf "\n%s\n" "$cert" 103 | for ((i = 0; i < ${#cert}; i++)); do echo -n "="; done 104 | echo 105 | curl --silent "$cert" || exit $? 106 | } >> "$output" 107 | done 108 | 109 | echo "Successfully created certfile \"$output\"." 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | - [The Script "gogcheck"](#gogcheck) 3 | - [Modify Script Variables](#modify-script-variables) 4 | - [The Script "makecertfile"](#makecertfile) 5 | - [For Windows Users](#for-windows-users) 6 | - [Cygwin and Git Bash](#cygwin-and-git-bash) 7 | - [WSL](#wsl) 8 | - [Q&A](#qa) 9 | 10 | # gogcheck 11 | 12 | Bash script for the purpose of scanning your GOG offline installer collection for valid digital signatures and correct checksums, making sure your downloads have not been modified by someone else. 13 | 14 | Usage: `gogcheck [options] [file/directory ...]` 15 | The script accepts multiple .exe files and directories as arguments. If no files or directories are specified, the current directory will be used. If neither the -s, -b/-B, nor -i/-I option is used, all checks will be run. 16 | 17 | Options: 18 | ``` 19 | -b Enable bin files check 20 | -B Same as -b, but disable checksum calculation 21 | -c Compact mode: all output but filenames and results is suppressed 22 | -1 Same as -c 23 | -C Disable colors 24 | -f Force checks on all exe files (not just setup_*.exe and patch_*.exe) 25 | -h Display this help 26 | -i Enable Inno Setup check 27 | -I Same as -i, but disable test-extracting 28 | -r Traverse directories recursively 29 | -R Disable RAR test-extracting 30 | -s Enable exe digital signature verification 31 | -S Silent mode: all output is suppressed; only the 1st exe file is checked 32 | (Used for exit code checks) 33 | -- Anything following this is considered a file/directory 34 | ``` 35 | 36 | The script consists of 3 functions, which run in this order: 37 | 1. sigcheck: checks .exe files for valid digital signatures 38 | 2. bincheck: checks if .bin files' checksums (which the .exe contains) are valid (only means something if sigcheck succeeds) 39 | 3. innocheck: test-extracts game files from both .exe and .bin files and verifies their checksums (sometimes which the .exe contains) 40 | 41 | Sample output: 42 | ``` 43 | $ gogcheck setup_a_corrupted_game.exe 44 | [1] setup_a_corrupted_game.exe 45 | Running signature check... 46 | Current PE checksum : 0E22C4AF 47 | Calculated PE checksum: 0E22C4DF MISMATCH!!! 48 | Signature Index: 0 (Primary Signature) 49 | Message digest algorithm : SHA1 50 | Current message digest : C421540390F3ACE7D031A4D54F0E5CA539D866AD 51 | Calculated message digest : EEF389073A91EE3470FCC7B5EE0CCDFF7A54BD22 MISMATCH!!! 52 | Signature verification: failed 53 | Number of verified signatures: 1 54 | Failed 55 | Running bin check... 56 | Exe file claims not to have bin files. 57 | No matching bin files found. 58 | Running innoextract check... 59 | 117 files (404.23 MiB) 60 | 117 checksums (105 SHA-1, 12 MD5) 61 | Test-extracting files... 62 | Extraction successful. 63 | 64 | 1 file checked, 1 error 65 | 66 | Files that produced errors: 67 | [1] setup_a_corrupted_game.exe (digital signature) 68 | ``` 69 | 70 | Sample output (compact mode): 71 | ``` 72 | $ gogcheck -1 73 | [1] ./setup_a_corrupted_game.exe Error 74 | [2] ./setup_ftl_advanced_edition_1.6.13b_(36400).exe OK 75 | [3] ./setup_terraria_v1.4.1.2_(42619).exe OK 76 | [4] ./setup_the_witcher_adventure_game_1.2.5a_(12082).exe OK 77 | 78 | 4 files checked, 1 error 79 | 80 | Files that produced errors: 81 | [1] ./setup_a_corrupted_game.exe (digital signature) 82 | ``` 83 | 84 | Required programs: 85 | - [osslsigncode](https://github.com/mtrojnar/osslsigncode) 86 | - [innoextract](https://github.com/dscharrer/innoextract) (at least version 1.5 for RAR support) 87 | 88 | Optional: 89 | - [unrar](https://www.rarlab.com/rar_add.htm) to let innoextract test RAR archives 90 | 91 | ## Modify Script Variables 92 | To optionally specify a Certificate Authority (CA) file (also see [makecertfile](#makecertfile)) or to override default program names, edit the script's "USER VARIABLES" section or pass them as command line prefixes: 93 | 94 | certfile=/etc/ssl/certs/ca-certificates.crt osslsigncode_binary=/usr/local/bin/osslsc_2.7 innoextract_binary=inno_1.9 unrar_directory=~/bin gogcheck ... 95 | 96 | --- 97 | gogcheck may still have bugs. Please report issues at https://github.com/hippie68/gogcheck/issues. Any feedback is very welcome! 98 | 99 | # makecertfile 100 | 101 | Sometimes new GOG installers may be signed by new certificates that aren't included in the provided (or automatically used) certificate authorities file, causing the following error in osslsigncode/sigcheck: 102 | 103 | ``` 104 | Error: unable to get local issuer certificate 105 | PKCS7_verify error 106 | ... 107 | ``` 108 | The optional "makecertfile" script can be used to create an up-to-date certificate file. 109 | The script downloads files from external servers: Mozilla's certificate list and additional certificates whose URLs can be added to the script (separated by newlines). 110 | Be aware the script downloads and executes the third party script "mk-ca-bundle.pl" from the cURL GitHub repository. 111 | 112 | Usage: `makecertfile OUTPUT_FILENAME` 113 | The script will generate a new certificate file named "OUTPUT_FILENAME". 114 | See [Modify Script Variables](#modify-script-variables) for how to make gogcheck aware of the newly-generated file. 115 | 116 | Required programs: 117 | - [cURL](https://github.com/curl/curl) 118 | - [Perl](https://github.com/Perl/perl5) 119 | 120 | Optional: 121 | - [sha256sum](https://www.gnu.org/software/coreutils) to let makecertfile verify mk-ca-bundle.pl's integrity 122 | 123 | # For Windows Users 124 | 125 | The script is confirmed to work with either of the following setups: 126 | 127 | - [Cygwin](https://www.cygwin.com) 128 | - [Git Bash](https://gitforwindows.org) 129 | - [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl) (WSL) on Windows 10/11 130 | 131 | ## Cygwin and Git Bash 132 | You can use the required programs' Windows versions (.exe files). Note that curl.exe is already included in Windows 10/11. 133 | 134 | ## WSL 135 | You can install the required programs' Linux versions in WSL like this: 136 | ``` 137 | sudo apt install osslsigncode innoextract unrar curl perl coreutils 138 | ``` 139 | Mark the scripts as executable: `chmod +x gogcheck makecertfile`. 140 | To run gogcheck from anywhere inside WSL, for example put it in `/usr/local/bin`: `cp gogcheck /usr/local/bin`. 141 | When editing the scripts from within Windows, make sure the editor you are using is respecting the Unix/Linux "LF" newline character format. On a recent, fully updated Windows 10/11 build, notepad.exe can be used. To instead edit the scripts from within WSL, use a Linux editor (for example nano: `nano gogcheck`; save with Ctrl-x). 142 | 143 | # Q&A 144 | What does it mean if sigcheck's output goes green? 145 | 146 | - It means the string that went green is known to the script. The latter which contains a section in which you can put known-legit strings found in your purchased games. This pre-made string collection is not complete. However, as this optional feature is just there for visual convenience, to quickly spot both known and new strings, it does not affect osslsigncode's functionality. 147 | 148 | Is RAR support required? 149 | 150 | - As innoextract does not know checksums for files stored inside RAR bin files (as opposed to Inno Setup bin files), the verification chain "valid .exe digital signature -> verified .bin checksums -> verified .bin archive contents" is broken at the final stage. You can still let innoextract use unrar/unar to check for regular CRC errors. 151 | -------------------------------------------------------------------------------- /gogcheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script tests GOG .exe and .bin files for authenticity and file integrity. 4 | # It can run 3 checks: digital signature verification, .bin checksum verification, 5 | # and Inno Setup checksum verification (testing the actual game files). 6 | # The goal is to ensure game files haven't been tempered with by a 3rd party. 7 | # 8 | # The following programs are required: 9 | # - osslsigncode (e.g. https://github.com/mtrojnar/osslsigncode) 10 | # - innoextract (https://constexpr.org/innoextract/) 11 | # 12 | # Optional: 13 | # - unrar/unar to test-extract RAR archives 14 | # 15 | # If errors have been encountered, the scripts exits with exit code 1. 16 | # 17 | # This script is considered work-in-progress and expected to have bugs. 18 | # Please report any issues at https://github.com/hippie68/gogcheck 19 | 20 | # USER VARIABLES: ############################################################# 21 | 22 | enable_colors=true 23 | compactmode=false 24 | 25 | # You might need to specify a Certificate Authority (CA) file for osslsigncode. 26 | # On Debian/Ubuntu/WSL: "/etc/ssl/certs/ca-certificates.crt" 27 | # For Git Bash: "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" 28 | # Or: "/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt" 29 | #certfile= 30 | 31 | # Optionally specify the required programs' locations here: 32 | #osslsigncode_binary= 33 | #innoextract_binary= 34 | #unrar_directory= 35 | 36 | # The variables "subjects", "issuers", and "serials" are optional lists 37 | # of known/trusted strings, which then appear colored in the signature verification. 38 | # For "subjects" and "serials", first block is GOG, then DigiCert, Sectigo each. 39 | # It is not a complete collection, and future game releases might come with new strings. 40 | # Use this section to include additional strings seen in your known-legit files. 41 | 42 | subjects=' 43 | /C=PL/L=Warsaw/O=GOG Sp. z o.o./CN=GOG Sp. z o.o. 44 | /C=PL/L=Warszawa/O=GOG Sp. z o.o./CN=GOG Sp. z o.o. 45 | /C=CY/L=Larnaca/O=GOG Limited/CN=GOG Limited 46 | /C=CY/L=Nicosia/O=GOG Limited/CN=GOG Limited 47 | /C=PL/ST=Mazowieckie/L=Warszawa/O=GOG Sp. z o.o./CN=GOG Sp. z o.o. 48 | /C=PL/ST=MAZOWIECKIE/L=WARSZAWA/O=GOG sp. z o.o/CN=GOG sp. z o.o 49 | 50 | /C=US/O=DigiCert/CN=DigiCert Timestamp Responder 51 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Code Signing CA-1 52 | /C=US/O=DigiCert, Inc./CN=DigiCert Timestamp 2021 53 | /C=US/O=DigiCert/CN=DigiCert Timestamp 2022 - 2 54 | 55 | /C=GB/O=Sectigo Limited/CN=Sectigo Public Time Stamping CA R36 56 | /C=GB/ST=Manchester/O=Sectigo Limited/CN=Sectigo Public Time Stamping Signer R35 57 | /C=GB/O=Sectigo Limited/CN=Sectigo Public Time Stamping Root R46 58 | /C=GB/ST=West Yorkshire/O=Sectigo Limited/CN=Sectigo Public Time Stamping Signer R36 59 | ' 60 | 61 | issuers=' 62 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Assured ID Code Signing CA 63 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA 64 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID CA-1 65 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Assured ID Timestamping CA 66 | /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Trusted Root G4 67 | /C=US/O=DigiCert, Inc./CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1 68 | /C=US/O=DigiCert, Inc./CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA 69 | 70 | /C=GB/O=Sectigo Limited/CN=Sectigo Public Time Stamping Root R46 71 | /C=GB/O=Sectigo Limited/CN=Sectigo Public Time Stamping CA R36 72 | /C=US/ST=New Jersey/L=Jersey City/O=The USERTRUST Network/CN=USERTrust RSA Certification Authority 73 | ' 74 | 75 | serials=' 76 | 06F56DD38538018E9A31248796E640AB 77 | 0B2CE86937CD32092D0C003EFDF5D988 78 | 0CB6E45E4D9295AFD0DEA595E0156ACE 79 | 0DB093590538192F52B39C94119A48CB 80 | 05B5D9D6BB2960FBD330C5D6B9B7B7D2 81 | 087DE552D352D9BAF0C2B6DF6AC4C64A 82 | 096DCF2E35C66F13EF95FCC8BFAC3E11 83 | 0B17A63F5D10CB7D3B78AF8F676C7667 84 | 0B976DD3814E722F750A35F3BC91BFA0 85 | 0BAD5D6BF5CE1EF257DAFB8B75BE92B2 86 | 0BB365E0AA6AC0BB473F5E4122062BD6 87 | 88 | 0409181B5FD5BB66755343B56F955008 89 | 03019A023AFF58B16BD6D5EAE617F066 90 | 06FDF9039603ADEA000AEB3F27BBBA1B 91 | 038B96F070D9E21E55A5426792E1C83A 92 | 0FA8490615D700A0BE2176FDC5EC6DBD 93 | 039FEDEDCB795B8DED320C8919F03689 94 | 0D424AE0BE3A88FF604021CE1400F0DD 95 | 0AA125D6D6321B7E41E405DA3697C215 96 | 0E9B188EF9D02DE7EFDB50E20840185A 97 | 073637B724547CD847ACFD28662A5E5B 98 | 08AD40B260D29C4C9F5ECDA9BD93AED9 99 | 0C4D69724B94FA3C2A4A3D2907803D5A 100 | 0CE7E0E517D846FE8FE560FC1BF03039 101 | 059B1B579E8E2132E23907BDA777755C 102 | 103 | 7A23AEDA5369960F91C83E5CF4C7E33F 104 | 3A526A2C84CE55E61D65FCCC12D8E989 105 | 36C2B0BD7C1B3AE7A3B3DD36CBC97568 106 | 01FD6D30FCA3CA51A81BBC640E35032D 107 | A4293B6E1EDDD7A7340887AD7A4EB724 108 | ' 109 | 110 | set_colors() { 111 | color_success='\033[92m' # Bright green 112 | color_error='\033[91m' # Bright red 113 | color_known='\033[32m' # Green 114 | color_unknown='\033[33m' # Yellow 115 | color_file='\033[1;95m' # Bold bright purple 116 | color_check='\033[95m' # Bright purple 117 | color_reset='\033[0;39m' 118 | color_debug1='\033[1;96m' # Bold cyan 119 | color_debug2='\033[96m' # Cyan 120 | } 121 | 122 | # INITIALIZATION: ############################################################# 123 | 124 | # "okstrings" and "errorstrings" are used to highlight osslsigncode's results 125 | okstrings='Signature verification: ok|Signature CRL verification: ok' 126 | errorstrings='MISMATCH!!!!|MISMATCH!!!|MISMATCH|failed|Failed|No signature found' 127 | 128 | rarcount=0 # Global number of RAR archives encountered by innocheck() 129 | rarfiles="" # List of .bin files that are identified as RAR archives 130 | errorcount=0 # Global count of errors or unexpected results 131 | errorfiles="" # List of files that produced errors 132 | checkcount=0 # Each .exe file gets a unique number - used for tracking errors faster 133 | exe_regex='(^|.*/)(setup_|patch_).*\.exe' # Overridden by command line option -F 134 | maxdepth='-maxdepth 1' # Standard value for main loop's "find" command, unless -r set 135 | unraroption='--gog' # Enables RAR test-extracting 136 | 137 | usage() { 138 | echo "Usage: ${0//*\/} [options] [file/directory ...] 139 | 140 | Checks your GOG offline installer collection for valid digital signatures and correct checksums, making sure the files are legit and have not been tempered with. 141 | If no files or directories are specified, the current directory will be used. 142 | If neither the -s, -b/-B, nor -i/-I option is used, all checks will be run. 143 | 144 | The checks consist of 3 parts: 145 | 1. Digital signature verification for .exe files 146 | 2. File checksum verification for .bin files 147 | 3. Inno Setup file checksum verification (actual game files packed inside .exe and .bin files) 148 | 149 | Options: 150 | -b Enable binary check 151 | -B Same as -b, but disable checksum calculation 152 | -c Compact mode: all output but filenames and results is suppressed 153 | -1 Same as -c 154 | -C Disable colors 155 | -f Force checks on all .exe files (not just setup_*.exe and patch_*.exe) 156 | -h Display this help 157 | -i Enable innoextract check 158 | -I Same as -i, but disable test-extracting 159 | -r Traverse directories recursively 160 | -R Disable RAR test-extracting 161 | -s Enable signature check 162 | -S Silent mode: all output is suppressed; only the 1st .exe file is checked 163 | (Used for exit code checks) 164 | -- Anything following this is considered a file/directory 165 | " 166 | exit 0 167 | } 168 | 169 | # Parsing command line arguments 170 | while [[ $1 == "-"* && ${#1} -gt 1 ]]; do 171 | if [[ $1 == "--" ]]; then shift; break; fi 172 | for ((i=1;i<${#1};i++)); do 173 | case ${1:i:1} in 174 | b) check_binaries=true ;; 175 | B) check_binaries=true; disable_binchecksumcalculation=true ;; 176 | c|1) compactmode=true ;; 177 | C) unset enable_colors ;; 178 | D) debug=true ;; 179 | f|F) exe_regex='.*\.exe' ;; 180 | h) usage ;; 181 | i) check_innosetup=true ;; 182 | I) check_innosetup=true; disable_extracting=true ;; 183 | r) unset maxdepth ;; 184 | R) unset unraroption ;; 185 | s) check_signatures=true ;; 186 | S) silentmode=true; exec &> /dev/null ;; 187 | *) echo "${0//*\/}: invalid option: '${1:i:1}'"; usage ;; 188 | esac 189 | done 190 | shift 191 | done 192 | 193 | # If no arguments have been given, use current directory 194 | [[ $1 == "" ]] && set -- . 195 | 196 | # If no check has been enabled, enable all checks 197 | if [[ $check_signatures$check_binaries$check_innosetup == "" ]]; then 198 | check_signatures=true check_binaries=true check_innosetup=true 199 | fi 200 | 201 | # Set program locations 202 | if [[ $check_signatures == true ]]; then 203 | [[ ! $osslsigncode_binary ]] && osslsigncode_binary=osslsigncode 204 | if ! hash "$osslsigncode_binary" &> /dev/null; then 205 | echo "osslsigncode binary not found! Which is required to verify .exe signatures." 206 | echo "Download the latest version from https://github.com/mtrojnar/osslsigncode." 207 | exit 1 208 | fi 209 | fi 210 | if [[ $check_innosetup == true ]]; then 211 | [[ ! $innoextract_binary ]] && innoextract_binary=innoextract 212 | if ! hash "$innoextract_binary" &> /dev/null; then 213 | echo "innoextract binary not found! Which is required to verify checksums of compressed files inside .exe and .bin files." 214 | echo "Download the latest version from https://constexpr.org/innoextract." 215 | exit 1 216 | fi 217 | fi 218 | [[ $unrar_directory ]] && PATH="$unrar_directory:$PATH" 219 | 220 | # Check for "md5sum" command 221 | if [[ $check_binaries == true ]] && ! hash md5sum &> /dev/null; then 222 | echo "The \"md5sum\" command is not available on this system. It is required to verify .bin files." 223 | exit 1 224 | fi 225 | 226 | # Check for "file" command 227 | if ! file -v &> /dev/null; then 228 | echo "The \"file\" command is not available on this system. RAR files cannot be detected and will produce errors during the innoextract check." 229 | else 230 | file_command_available=1 231 | fi 232 | 233 | # Disable RAR extraction if rar/unar not available 234 | if [[ ! -z ${unraroption+x} && $check_innosetup == true && ! $disable_extracting == true ]] \ 235 | && ! hash unrar &> /dev/null && ! hash unar &> /dev/null; then 236 | unset unraroption 237 | echo "unrar/unar binary not found - RAR extraction has been disabled." 238 | echo "Download the latest UnRAR version from https://www.rarlab.com/rar_add.htm." 239 | fi 240 | 241 | # Set osslsigncode options 242 | osslsigncode_version=$("$osslsigncode_binary" -v 2> /dev/null \ 243 | | grep -Po 'osslsigncode \K[^,]*(?=,)') 244 | if [[ ${#osslsigncode_version} -eq 3 ]]; then 245 | osslsigncode_version=${osslsigncode_version//./0} 246 | else 247 | osslsigncode_version=${osslsigncode_version//./} 248 | fi 249 | if [[ $certfile && $osslsigncode_version -ge 201 ]]; then 250 | osslsigncode_options="-CAfile $certfile -untrusted $certfile" 251 | # Note: future versions will have changed "-untrusted" to "-TSA-CAfile", but 252 | # still keep the old name for compatibility. 253 | fi 254 | 255 | # Enable colors 256 | [[ $enable_colors == true ]] && set_colors 257 | 258 | # Converts user strings to osslsigncode 2.10+ output, if necessary 259 | convert_to_210 () { 260 | IFS=$'\n' 261 | for line in $1; do 262 | if [[ ${line:0:1} == '/' ]]; then 263 | IFS=/ 264 | local tokens=(${line:1}) 265 | line= 266 | for ((i = ${#tokens[@]} - 1; i >= 0; i--)); do 267 | tokens[i]=${tokens[i]//,/'\\',} 268 | line+=${tokens[i]} 269 | [[ $i -ne 0 ]] && line+=, 270 | done 271 | fi 272 | echo "$line" 273 | done 274 | } 275 | if [[ $osslsigncode_version -ge 210 ]]; then 276 | subjects=$(convert_to_210 "$subjects") 277 | issuers=$(convert_to_210 "$issuers") 278 | fi 279 | 280 | # Escapes all regex metacharacters in user strings for internal regex use, and 281 | # creates "|"-delimited strings 282 | prepare_user_strings() { 283 | IFS=$'\n' 284 | for string in $1; do 285 | for char in \[ \] \( \) \. \^ \$ \? \* \+ \|; do 286 | string=${string//"$char"/"\\$char"} 287 | done 288 | result+="$string|" 289 | done 290 | echo "${result%|}" 291 | } 292 | subjects=$(prepare_user_strings "$subjects") 293 | issuers=$(prepare_user_strings "$issuers") 294 | serials=$(prepare_user_strings "$serials") 295 | 296 | # FUNCTIONS: ################################################################## 297 | 298 | debugmessage() { 299 | if [[ $debug == true ]]; then 300 | echo -e "${color_debug1}DEBUG MESSAGE: ${color_debug2}$1${color_reset}" 301 | fi 302 | } 303 | 304 | # Used for logging errors; argument format: "filename" "reason" 305 | error() { ((errorcount++)); errorfiles+="[$checkcount] $1 ($2)"$'\n'; } 306 | 307 | sigcheck() { 308 | echo -e "${color_check}Running signature check...${color_reset}" 309 | local osslsigncode_output 310 | osslsigncode_output=$("$osslsigncode_binary" verify $osslsigncode_options "$1" 2>&1) 311 | returncode=$? 312 | 313 | # Adjust osslsigncode's output 314 | if [[ $compactmode != true ]]; then 315 | while read -r line; do # read without "IFS=" removes ossligncode's tabs 316 | # Mark known/unknown strings 317 | if [[ $line == +(Subject:|Issuer :|Serial :)* ]]; then 318 | if [[ ${line#*: } == +($subjects|$issuers|$serials) ]]; then 319 | echo -e "${line%%:*}:${color_known}${line#*:}${color_reset}" 320 | else 321 | echo -e "${line%%:*}:${color_unknown}${line#*:}${color_reset}" 322 | fi 323 | # Mark errors 324 | elif [[ $line =~ .*($errorstrings) ]]; then 325 | echo -e "${color_error}$line${color_reset}" 326 | # Mark success 327 | elif [[ $line == *+($okstrings) ]]; then 328 | echo -e "${line:0:-2}${color_success}OK${color_reset}" 329 | # Remove osslsigncode clutter 330 | elif [[ $line == !(''|------------------|Succeeded|Succeeded.) \ 331 | && $line != +(Certificate expiration date:|notBefore|notAfter|CAfile:|TSA\'s certificates file|CRL distribution point|TSA\'s CRL distribution point:)* ]]; 332 | then 333 | echo "$line" 334 | fi 335 | done < <(echo "$osslsigncode_output") 336 | fi 337 | 338 | # Error handling 339 | # osslsigncode 2.0.0 and older: "No signature found." 340 | # osslsigncode 2.1.0: "No signature found" 341 | # Note: osslsigncode 2.0.0 and older exit with 0 even when no signature found 342 | if [[ $osslsigncode_output == *"No signature found"* ]]; then 343 | error "$1" "no digital signature found" 344 | elif [[ $returncode != 0 ]]; then 345 | error "$1" "digital signature error" 346 | elif [[ $osslsigncode_version -lt 210 && $osslsigncode_output != *'/O=GOG '* ]] \ 347 | || [[ $osslsigncode_version -ge 210 && $osslsigncode_output != *',O=GOG '* ]]; then 348 | echo -e "${color_error}Warning: digital signature not from GOG?${color_reset}" 349 | error "$1" "digital signature not from GOG?" 350 | fi 351 | } 352 | 353 | bincheck() { 354 | echo -e "${color_check}Running binary check...${color_reset}" 355 | local crcstring 356 | crcstring=$(tac "$1" | grep -a -m 1 -i -o '[[:alnum:]]*#GOGCRCSTRING') 357 | debugmessage "Raw GOGCRCSTRING: $crcstring" 358 | 359 | # Leave if no checksums found 360 | if [[ $crcstring == "" ]]; then 361 | echo -e "No .bin file checksums found." 362 | if compgen -G "${1%.exe}-*.bin" > /dev/null; then 363 | echo "However, matching .bin file(s) exist:" 364 | for i in "${1%.exe}"-*.bin; do echo " $i"; done 365 | echo -e "${color_error}Without checksums, cannot verify the .bin file(s)!${color_reset}" 366 | error "$1" ".bin checksums missing" 367 | else 368 | echo "No matching .bin files found either." 369 | echo -e "${color_error}Is this .exe a GOG installer?${color_reset}" 370 | error "$1" "not a GOG installer?" 371 | fi 372 | return 373 | fi 374 | 375 | crcstring=${crcstring%#GOGCRCSTRING*} # Remove the string name, leaving numbers 376 | local multiplier=$((10#${crcstring: -2})) # The 2-digit number before '#GOGCRCSTRING' 377 | 378 | # Leave if installer expects 0 .bin files 379 | if [[ $multiplier -eq 0 ]]; then 380 | echo "Installer claims not to have .bin files." 381 | if compgen -G "${1%.exe}-*.bin" > /dev/null; then 382 | echo -e "${color_unknown}However, matching .bin file(s) exist:${color_reset}" 383 | for i in "${1%.exe}"-*.bin; do echo " $i"; done 384 | error "$1" ".bin files exist that shouldn't" 385 | else 386 | echo "No matching .bin files found." 387 | fi 388 | return 389 | fi 390 | 391 | crcstring=${crcstring::-2} # Remove the 2-digit number 392 | local md5hashes=${crcstring:(( - $multiplier * 32 ))} # Omit characters not part of the hashes 393 | md5hashes=${md5hashes,,} # Set md5 hashes to lowercase 394 | debugmessage "Raw MD5 hashes: $md5hashes" 395 | echo -n "Found $multiplier checksum" 396 | if [[ $multiplier -eq 1 ]]; then echo ":"; else echo "s:"; fi 397 | 398 | # Get the checksums 399 | local checksum checksums 400 | for i in $(seq 1 $multiplier); do 401 | checksum=${md5hashes:(( 32 * (($i - 1)) )):32} 402 | [[ $checksum == "" ]] && checksum="[empty] " 403 | echo "$checksum" 404 | checksums+="$checksum " # Add current checksum to known checksums 405 | done 406 | 407 | # Compare the checksums 408 | local -i bincount=0 409 | if [[ $disable_binchecksumcalculation != true ]]; then 410 | if [[ $multiplier -eq 1 ]]; then 411 | echo "Verifying checksum..." 412 | else 413 | echo "Verifying checksums..." 414 | fi 415 | fi 416 | if compgen -G "${1%.exe}-*.bin" > /dev/null; then 417 | for bin in "${1%.exe}"-*.bin; do 418 | ((bincount++)) 419 | if [[ $disable_binchecksumcalculation != true ]]; then 420 | echo "${bin##*/}: " 421 | checksum=$(md5sum "$bin") 422 | checksum=${checksum%% *} 423 | echo -n "$checksum: " 424 | if [[ $checksums == *"$checksum"* ]]; then 425 | echo -e "${color_success}OK${color_reset}" 426 | else 427 | echo -e "${color_error}Error${color_reset}" 428 | error "${bin##*/}" "wrong checksum" 429 | fi 430 | else 431 | echo "Found .bin file: ${bin##*/}" 432 | fi 433 | done 434 | fi 435 | 436 | # Throw an error when the numbers of checksums and .bin files differ 437 | if [[ ! $multiplier -eq $bincount ]]; then 438 | echo -e "${color_error}Warning: $multiplier .bin files expected, but $bincount found.${color_reset}" 439 | error "$1" "wrong number of .bin files" 440 | fi 441 | } 442 | 443 | innocheck() { 444 | echo -e "${color_check}Running innoextract check...${color_reset}" 445 | 446 | # Abort if .exe is not an Inno Setup installer 447 | if ! "$innoextract_binary" --info "$1" > /dev/null; then 448 | echo -e "${color_error}innoextract reported errors while probing the file.${color_reset}" 449 | error "$1" "innoextract file probing" 450 | return 451 | fi 452 | 453 | # Display archive's filesize and checksum summary 454 | local -i filesize=0 filecount=0 sha1count=0 md5count=0 455 | while read -r -a array; do 456 | # 0: file size, 1: checksum type, 2: checksum, 3: file name 457 | if [[ ${array[0]} == +([[:digit:]]) ]]; then 458 | ((filesize+=array[0])) 459 | ((filecount++)) 460 | fi 461 | case ${array[1]} in 462 | SHA-1) ((sha1count++));; 463 | MD5) ((md5count++));; 464 | esac 465 | done < <("$innoextract_binary" --list-sizes --list-checksums --silent "$1" 2> /dev/null) 466 | filesize=$((100*filesize/1024/1024)) 467 | echo "$filecount files (${filesize%??}.${filesize: -2} MiB)" # Floating-point hack 468 | echo "$((sha1count + md5count)) checksums ($sha1count SHA-1, $md5count MD5)" 469 | if [[ ! $filecount -eq $((sha1count + md5count)) ]]; then 470 | echo "${color_error}Warning: Numbers of files and checksums differ.${color_reset}" 471 | error "$1" "innoextract checksum info" 472 | fi 473 | 474 | # Detect RAR archives 475 | if [[ $file_command_available && -f "${1%.exe}"-1.bin ]]; then 476 | if [[ $(file "${1%.exe}"-1.bin) == *RAR* ]]; then 477 | echo -e "${color_unknown}RAR format detected.${color_reset} Innoextract does not know .bin contents' checksums." 478 | if [[ $disable_extracting != true && $unraroption != '--gog' ]]; then 479 | echo "RAR extraction (--gog option) is disabled." 480 | fi 481 | ((rarcount++)) 482 | rarfiles+="$1"$'\n' 483 | fi 484 | fi 485 | 486 | # Test-extract Inno Setup archive 487 | if [[ $disable_extracting != true ]]; then 488 | echo "Test-extracting files..." 489 | if ! "$innoextract_binary" $unraroption --test --silent --progress=1 "$1"; then 490 | echo -e "${color_error}innoextract reported errors.${color_reset}" 491 | error "$1" "innoextract extraction" 492 | return 493 | else 494 | echo -e "Extraction successful." 495 | fi 496 | fi 497 | } 498 | 499 | scriptresult() { 500 | [[ $checkcount -eq 0 ]] && exit 1 501 | if [[ $checkcount -eq 1 ]]; then 502 | echo -n "1 file checked, " 503 | else 504 | echo -n "$checkcount files checked, " 505 | fi 506 | if [[ $errorcount -eq 0 ]]; then 507 | echo -en "${color_success}" 508 | else 509 | echo -en "${color_error}" 510 | fi 511 | if [[ $errorcount -eq 1 ]]; then 512 | echo -e "1 error${color_reset}" 513 | else 514 | echo -e "$errorcount errors${color_reset}" 515 | fi 516 | echo 517 | if [[ $rarcount -gt 0 ]]; then 518 | if [[ $unraroption == "" ]]; then 519 | echo "Files with (untested) RAR archive .bin files:" 520 | else 521 | echo "Files with RAR archive .bin files:" 522 | fi 523 | echo "$rarfiles" 524 | fi 525 | if [[ $errorcount -gt 0 ]]; then 526 | echo "Files that produced errors:" 527 | echo -e "${color_error}$errorfiles${color_reset}" 528 | exit 1 529 | fi 530 | exit 0 531 | } 532 | 533 | # Catch CTRL-C commands 534 | trap '{ [[ $compactmode == true ]] && { exec 1>&3; exec 2>&4; } 535 | echo " Script aborted."; echo; scriptresult; }' SIGINT 536 | 537 | gogcheck() { 538 | local error="$errorcount" # Used in compact mode, see bottom 539 | 540 | # Display currently checked .exe file 541 | ((checkcount++)) 542 | if [[ $compactmode == true ]]; then 543 | echo -en "[$checkcount] ${1} " 544 | exec 3>&1; exec 4>&2 # Save file descriptors 1 and 2 for later 545 | exec &> /dev/null # Disable output in compact mode 546 | else 547 | echo -e "[$checkcount] ${color_file}$1${color_reset}" 548 | fi 549 | 550 | # Run main checks on .exe file 551 | [[ $check_signatures == true ]] && sigcheck "$1" 552 | [[ $check_binaries == true ]] && bincheck "$1" 553 | [[ $check_innosetup == true ]] && innocheck "$1" 554 | 555 | # Make sure only 1 .exe file in total is checked in silent mode 556 | [[ $silentmode == true ]] && scriptresult 557 | 558 | # Display compact mode result 559 | if [[ $compactmode == true ]]; then 560 | exec 1>&3; exec 2>&4 # Re-enable output in compact mode 561 | if [[ $error -lt $errorcount ]]; then 562 | echo -en "${color_error}Error${color_reset}" 563 | else 564 | echo -en "${color_success}OK${color_reset}" 565 | fi 566 | fi 567 | 568 | echo 569 | } 570 | 571 | # MAIN LOOP: ################################################################## 572 | 573 | shopt -s nocasematch 574 | while [[ $1 != "" ]]; do 575 | if [[ -d "$1" ]]; then 576 | while IFS= read -r -d $'\0' exe; do 577 | gogcheck "$exe" 578 | done < <(find "$1" $maxdepth -regextype posix-extended -iregex "$exe_regex" -type f -print0 | sort -z) 579 | elif [[ -f "$1" ]] && [[ ${1,,} =~ $exe_regex ]]; then 580 | gogcheck "$1" 581 | fi 582 | shift 583 | done 584 | 585 | [[ $compactmode == true ]] && echo 586 | 587 | scriptresult 588 | --------------------------------------------------------------------------------