Browse Source

Merge branch 'issigkeys'

Martijn Laan 4 tháng trước cách đây
mục cha
commit
0025f0b9c0

+ 115 - 12
Components/ISSigFunc.pas

@@ -28,14 +28,29 @@ function ISSigCreateSignatureText(const AKey: TECDSAKey;
   const AFileSize: Int64; const AFileHash: TSHA256Digest): String;
 function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
   const AText: String; out AFileSize: Int64;
-  out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
+  out AFileHash: TSHA256Digest): TISSigVerifySignatureResult; overload;
+function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
+  const AText: String; out AFileSize: Int64;
+  out AFileHash: TSHA256Digest; out AKeyUsedID: String): TISSigVerifySignatureResult; overload;
 
 procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
   var APrivateKeyText: String);
 procedure ISSigExportPublicKeyText(const AKey: TECDSAKey;
   var APublicKeyText: String);
+procedure ISSigConvertPublicKeyToStrings(const APublicKey: TECDSAPublicKey;
+  out APublicX, APublicY: String);
+function ISSigParsePrivateKeyText(const AText: String;
+  out APrivateKey: TECDSAPrivateKey): TISSigImportKeyResult;
+function ISSigParsePublicKeyText(const AText: String;
+  out APublicKey: TECDSAPublicKey): TISSigImportKeyResult;
 function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
   const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
+function ISSigImportPublicKey(const AKey: TECDSAKey;
+  const AKeyID, APublicX, APublicY: String): TISSigImportKeyResult;
+
+procedure ISSigCheckValidKeyID(const AKeyID: String);
+procedure ISSigCheckValidPublicXOrY(const APublicXOrY: String);
+function ISSigIsValidKeyIDForPublicXY(const AKeyID, APublicX, APublicY: String): Boolean;
 
 function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
 
@@ -162,7 +177,7 @@ end;
 
 function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
   const AText: String; out AFileSize: Int64;
-  out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
+  out AFileHash: TSHA256Digest; out AKeyUsedID: String): TISSigVerifySignatureResult;
 var
   TextValues: record
     Format, FileSize, FileHash, KeyID, Sig_r, Sig_s: String;
@@ -172,9 +187,12 @@ begin
     properly checking the function result }
   AFileSize := -1;
   FillChar(AFileHash, SizeOf(AFileHash), 0);
+  AKeyUsedID := '';
 
   if Length(AText) > ISSigTextFileLengthLimit then
-    Exit(vsrMalformed);
+    Exit(vsrMalformed)
+  else if Length(AAllowedKeys) = 0 then
+    Exit(vsrKeyNotFound);
 
   var SS := TStringScanner.Create(AText);
   if not ConsumeLineValue(SS, 'format', TextValues.Format, 8, 8, NonControlASCIICharsSet) or
@@ -205,6 +223,7 @@ begin
   end;
   if KeyUsed = nil then
     Exit(vsrKeyNotFound);
+  AKeyUsedID := TextValues.KeyID;
 
   const UnverifiedFileSize = StrToInt64(TextValues.FileSize);
   const UnverifiedFileHash = SHA256DigestFromString(TextValues.FileHash);
@@ -220,6 +239,14 @@ begin
     Result := vsrBadSignature;
 end;
 
+function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
+  const AText: String; out AFileSize: Int64;
+  out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
+begin
+  var KeyUsedID: String;
+  Result := ISSigVerifySignatureText(AAllowedKeys, AText, AFileSize, AFileHash, KeyUsedID);
+end;
+
 procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
   var APrivateKeyText: String);
 begin
@@ -262,7 +289,15 @@ begin
   end;
 end;
 
-function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
+procedure ISSigConvertPublicKeyToStrings(const APublicKey: TECDSAPublicKey;
+  out APublicX, APublicY: String);
+begin
+  APublicX := ECDSAInt256ToString(APublicKey.Public_x);
+  APublicY := ECDSAInt256ToString(APublicKey.Public_y);
+end;
+
+function InternalParseKeyText(const AText: String;
+  out APrivateKey: TECDSAPrivateKey;
   const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
 var
   TextValues: record
@@ -294,25 +329,93 @@ begin
   if not SS.ReachedEnd then
     Exit;
 
-  var PrivateKey: TECDSAPrivateKey;
-  PrivateKey.PublicKey.Public_x := ECDSAInt256FromString(TextValues.Public_x);
-  PrivateKey.PublicKey.Public_y := ECDSAInt256FromString(TextValues.Public_y);
+  APrivateKey.Clear;  { just because Private_d isn't always set }
+  APrivateKey.PublicKey.Public_x := ECDSAInt256FromString(TextValues.Public_x);
+  APrivateKey.PublicKey.Public_y := ECDSAInt256FromString(TextValues.Public_y);
 
   { Verify that the key ID is correct for the public key values }
   if not SHA256DigestsEqual(SHA256DigestFromString(TextValues.KeyID),
-     CalcKeyID(PrivateKey.PublicKey)) then
+     CalcKeyID(APrivateKey.PublicKey)) then
     Exit;
 
   if ANeedPrivateKey then begin
     if not HasPrivateKey then
       Exit(ikrNotPrivateKey);
-    PrivateKey.Private_d := ECDSAInt256FromString(TextValues.Private_d);
-    AKey.ImportPrivateKey(PrivateKey);
-  end else
-    AKey.ImportPublicKey(PrivateKey.PublicKey);
+    APrivateKey.Private_d := ECDSAInt256FromString(TextValues.Private_d);
+  end;
+  Result := ikrSuccess;
+end;
+
+function ISSigParsePrivateKeyText(const AText: String;
+  out APrivateKey: TECDSAPrivateKey): TISSigImportKeyResult;
+begin
+  Result := InternalParseKeyText(AText, APrivateKey, True);
+end;
+
+function ISSigParsePublicKeyText(const AText: String;
+  out APublicKey: TECDSAPublicKey): TISSigImportKeyResult;
+begin
+  var PrivateKey: TECDSAPrivateKey;  { only PublicKey part is used }
+  Result := InternalParseKeyText(AText, PrivateKey, False);
+  if Result = ikrSuccess then
+    APublicKey := PrivateKey.PublicKey;
+end;
+
+function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
+  const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
+begin
+  var PrivateKey: TECDSAPrivateKey;
+  try
+    Result := InternalParseKeyText(AText, PrivateKey, ANeedPrivateKey);
+    if Result = ikrSuccess then begin
+      if ANeedPrivateKey then
+        AKey.ImportPrivateKey(PrivateKey)
+      else
+        AKey.ImportPublicKey(PrivateKey.PublicKey);
+    end;
+  finally
+    PrivateKey.Clear;
+  end;
+end;
+
+function ISSigImportPublicKey(const AKey: TECDSAKey;
+  const AKeyID, APublicX, APublicY: String): TISSigImportKeyResult;
+begin
+  var Publickey: TECDSAPublickey;
+  PublicKey.Public_x := ECDSAInt256FromString(APublicX);
+  PublicKey.Public_y := ECDSAInt256FromString(APublicY);
+
+  if AKeyID <> '' then begin
+    { Verify that the key ID is correct for the public key values }
+    if not SHA256DigestsEqual(SHA256DigestFromString(AKeyID),
+       CalcKeyID(PublicKey)) then
+      Exit(ikrMalformed);
+  end;
+
+  AKey.ImportPublicKey(PublicKey);
   Result := ikrSuccess;
 end;
 
+procedure ISSigCheckValidKeyID(const AKeyID: String);
+begin
+  SHA256DigestFromString(AKeyID);
+end;
+
+procedure ISSigCheckValidPublicXOrY(const APublicXOrY: String);
+begin
+  ECDSAInt256FromString(APublicXOrY);
+end;
+
+function ISSigIsValidKeyIDForPublicXY(const AKeyID, APublicX, APublicY: String): Boolean;
+begin
+  var PublicKey: TECDSAPublicKey;
+  PublicKey.Public_x := ECDSAInt256FromString(APublicX);
+  PublicKey.Public_y := ECDSAInt256FromString(APublicY);
+
+  Result := SHA256DigestsEqual(SHA256DigestFromString(AKeyID),
+     CalcKeyID(PublicKey));
+end;
+
 function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
 var
   Buf: array[0..$FFFF] of Byte;

+ 57 - 4
Examples/CodeDownloadFiles.iss

@@ -2,6 +2,15 @@
 ;
 ; This script shows how the CreateDownloadPage support function can be used to
 ; download temporary files while showing the download progress to the user.
+; One of the files is a 7-Zip archive and the script shows how to extract it.
+;
+; To verify the downloaded files, this script shows two methods:
+; -For innosetup-latest.exe and MyProg-ExtraReadmes.7z: using the Inno Setup
+;  Signature Tool, the [ISSigKeys] section, and the issigverify flag
+; -For iscrypt.dll: using a simple SHA256 check
+; Using the Inno Setup Signature Tool has the benefit that the script does not
+; need to be changed when the downloaded file changes, so any installers built
+; will also keep working
 
 [Setup]
 AppName=My Program
@@ -12,14 +21,21 @@ DefaultGroupName=My Program
 UninstallDisplayIcon={app}\MyProg.exe
 OutputDir=userdocs:Inno Setup Examples Output
 
+[ISSigKeys]
+Name: "mykey"; \
+  KeyID:   "def020edee3c4835fd54d85eff8b66d4d899b22a777353ca4a114b652e5e7a28"; \
+  PublicX: "515dc7d6c16d4a46272ceb3d158c5630a96466ab4d948e72c2029d737c823097"; \
+  PublicY: "f3c21f6b5156c52a35f6f28016ee3e31a3ded60c325b81fb7b1f88c221081a61"
+
 [Files]
 ; Place any regular files here
 Source: "MyProg.exe"; DestDir: "{app}";
 Source: "MyProg.chm"; DestDir: "{app}";
 Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
 ; These files will be downloaded
-Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external
-Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external
+Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external ignoreversion issigverify
+Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external ignoreversion
+Source: "{tmp}\MyProg-ExtraReadmes\*.txt"; DestDir: "{app}"; Flags: external recursesubdirs ignoreversion issigverify  
 
 [Icons]
 Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
@@ -27,6 +43,7 @@ Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
 [Code]
 var
   DownloadPage: TDownloadWizardPage;
+  ExtractionPage: TExtractionWizardPage;
 
 function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
 begin
@@ -35,10 +52,18 @@ begin
   Result := True;
 end;
 
+function OnExtractionProgress(const ArchiveName, FileName: String; const Progress, ProgressMax: Int64): Boolean;
+begin
+  if Progress = ProgressMax then
+    Log(Format('Successfully extracted archive to {tmp}\MyProg-ExtraReadmes: %s', [ArchiveName]));
+  Result := True;
+end;
+
 procedure InitializeWizard;
 begin
   DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress);
   DownloadPage.ShowBaseNameInsteadOfUrl := True;
+  ExtractionPage := CreateExtractionPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnExtractionProgress);
 end;
 
 function NextButtonClick(CurPageID: Integer): Boolean;
@@ -47,11 +72,14 @@ begin
     DownloadPage.Clear;
     // Use AddEx to specify a username and password
     DownloadPage.Add('https://jrsoftware.org/download.php/is.exe?dontcount=1', 'innosetup-latest.exe', '');
+    DownloadPage.Add('https://jrsoftware.org/download.php/is.exe.issig?dontcount=1', 'innosetup-latest.exe.issig', '');
     DownloadPage.Add('https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc');
+    DownloadPage.Add('https://jrsoftware.org/download.php/myprog-extrareadmes.7z?dontcount=1', 'MyProg-ExtraReadmes.7z', '');
     DownloadPage.Show;
     try
       try
-        DownloadPage.Download; // This downloads the files to {tmp}
+        // Downloads the files to {tmp}
+        DownloadPage.Download;
         Result := True;
       except
         if DownloadPage.AbortedByUser then
@@ -63,6 +91,31 @@ begin
     finally
       DownloadPage.Hide;
     end;
+    
+    if not Result then
+      Exit;
+      
+    ExtractionPage.Clear;
+    ExtractionPage.Add(ExpandConstant('{tmp}\MyProg-ExtraReadmes.7z'), ExpandConstant('{tmp}\MyProg-ExtraReadmes'), True);
+    ExtractionPage.Show;
+    try
+      try
+        // Extracts the archive to {tmp}\MyProg-ExtraReadmes
+        // Please see the Extract7ZipArchive topic in the help file for limitations before using this for you own archives
+        // Note that each file in this archive come with an .issig signature file
+        // These signature files are used by the [Files] section to verify the archive's content
+        ExtractionPage.Extract; 
+        Result := True;
+      except
+        if ExtractionPage.AbortedByUser then
+          Log('Aborted by user.')
+        else
+          SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
+        Result := False;
+      end;
+    finally
+      ExtractionPage.Hide;
+    end;
   end else
-    Result := True;
+    Result := True;    
 end;

+ 6 - 0
Examples/MyDll.dll.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 21280
+file-hash f4e549ef947c33a61520a38c855583e48cfd1702303815123662f7e2e4e73e09
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 180da3187e447b568029067128d435e679f5f6989aa64b39880ed73288bf7c5c
+sig-s 237b83598d51f7e205ca437223fbe65f78598774b006b309805e48445399d29c

+ 6 - 0
Examples/MyProg-Arm64.exe.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 77960
+file-hash ca7812155ff5935264177277b7c351ef35deacfda114fe8418d65cbcf105883a
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r bb674f9dedab8fa5ac52e2c7e7962b7ce259b8464d2c59b5fed1ae644a26ff9c
+sig-s c8f039a9d89c83704c47d94bf63515f635d452e54cefd61d99c50aa5e8fac257

+ 6 - 0
Examples/MyProg-x64.exe.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 77960
+file-hash b50952f9af310be586746356508b9fd430dcbbaa155a04e6f9563632de0a023b
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 5a2a353204285614686df1928a7953a78542a2eb83b129a8e97b526126948854
+sig-s fc65ef8728f1ac0b48ccaa228794191745ec361d2d92175c9aafac85f0aaffcd

+ 6 - 0
Examples/MyProg.exe.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 78984
+file-hash be272491050410c39db97101f05e80ed5d30a10caca6bc9f02228aa9499e19c9
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r c9b85f334dcadca472e016f297b3c4c76a55f6a3997727eaf140676bcc74792c
+sig-s df60e6c6e59d8934b0694e5682503e4c34918dd73f1cbb99897ebb18944ac834

+ 6 - 0
Files/isbunzip.dll.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 35616
+file-hash 31d04c1e4bfdfa34704c142fa98f80c0a3076e4b312d6ada57c4be9d9c7dcf26
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 460e8f09454711a91475bfecc3a1d13c18ca4041b75e79c8f863a9f6ac3f0855
+sig-s 398a131094d6d4965f616a1ae8d5fd7a3084fb2904a8cfc14ad659503887e4bb

+ 6 - 0
Files/islzma32.exe.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 199304
+file-hash 5665e03297af04d4f90423b56e8b016b2924d91ba781573036b710044e843f0a
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 53e7bc7e2f24dca57da19a5b921c0d8c693e3de6b70bf5e4eee8e46409885c82
+sig-s d0348f1cf9633d688163ecfb0af5f7213fd3f16270f8f515229ab05e08ed78f4

+ 6 - 0
Files/islzma64.exe.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 230536
+file-hash b0ad1857da7fb94b54988c7633578dc057b51cf921b94f6f2f7b57a113310712
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 12999c74a0cd2229d70f862efa4ea36acdaacc546148b60fc276ee4fa2de0dc4
+sig-s 11256db23361add72dee940e870bcad5dc179c77957d143a5a9201d70eb56499

+ 6 - 0
Files/isunzlib.dll.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 29472
+file-hash 8287d0e287a66ee78537c8d1d98e426562b95c50f569b92cea9ce36a9fa57e64
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r c3250004a73dc6ed3be3c4faad66aee0768e49a7b4d0a6f9ec3f03d1924b1411
+sig-s 00da1d34b90c23ca78515e71263c768e0df0a0923c3d7532223ea60f8bab4936

+ 234 - 10
ISHelp/isetup.xml

@@ -39,6 +39,7 @@
     <contentstopic title="[Run] section" topic="runsection" />
     <contentstopic title="[UninstallDelete] section" topic="uninstalldeletesection" />
     <contentstopic title="[UninstallRun] section" topic="runsection" />
+    <contentstopic title="[ISSigKeys] section" topic="issigkeyssection" />
   </contentsheading>
   <contentsheading title="Pascal Scripting">
     <contentstopic title="Introduction" topic="scriptintro" />
@@ -86,6 +87,7 @@
     <contentstopic title="Uninstaller Exit Codes" topic="uninstexitcodes" />
     <contentstopic title="Compiler IDE Keyboard And Mouse Commands" topic="compformshortcuts" />
     <contentstopic title="Compiler IDE Regular Expressions" topic="compformregex" />
+    <contentstopic title=".issig Signatures: Introduction" topic="issig" />
     <contentstopic title="Inno Setup Signature Tool" topic="issigtool" />
     <contentstopic title="Miscellaneous Notes" topic="technotes" />
     <contentstopic title="Example Scripts" topic="examples" />
@@ -103,7 +105,7 @@
 <body>
 
 <p>
-<b>Inno Setup version 6.4.3</b><br/>
+<b>Inno Setup version 6.5.0-dev</b><br/>
 <b>Copyright &copy; 1997-2025 Jordan Russell. All rights reserved.</b><br/>
 <b>Portions Copyright &copy; 2000-2025 Martijn Laan. All rights reserved.</b><br/>
 <extlink href="https://jrsoftware.org/">Inno Setup home page</extlink>
@@ -268,6 +270,7 @@ Source: "MYPROG.EXE"; DestDir: "{app}"
 <link topic="runsection">[Run] section</link><br/>
 <link topic="uninstalldeletesection">[UninstallDelete] section</link><br/>
 <link topic="runsection">[UninstallRun] section</link><br/>
+<link topic="issigkeyssection">[ISSigKeys] section</link><br/>
 <link topic="scriptintro">Pascal Scripting: Introduction</link><br/>
 <link topic="isppoverview">Inno Setup Preprocessor: Introduction</link>
 </p>
@@ -1648,6 +1651,13 @@ ExternalSize: 1048576; Flags: external
 </example>
 </param>
 
+<param name="ISSigAllowedKeys">
+<p>A space separated list of key names or group names from the <link topic="issigkeyssection">[ISSigKeys] section</link>, specifying which keys are allowed for the verification done by the <tt>issigverify</tt> flag.</p>
+<example>
+<pre>ISSigAllowedKeys: "exesigner"</pre>
+</example>
+</param>
+
 <param name="Flags">
 <p>This parameter is a set of extra options. Multiple options may be used by separating them by spaces. The following options are supported:</p>
 
@@ -1706,6 +1716,18 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 <p>Don't compare version info at all; replace existing files regardless of their version number.</p>
 <p>This flag should only be used on files private to your application, <i>never</i> on shared system files.</p>
 </flag>
+<flag name="issigverify">
+<p>Instructs the compiler or Setup to verify the source file's signature using a key from the <link topic="issigkeyssection">[ISSigKeys] section</link>, allowing all keys by default. Use the <tt>ISSigAllowedKeys</tt> parameter to limit the allowed keys.</p>
+<p>The verification requires an <tt>.issig</tt> signature file to be present in the same directory as the source file, created using the <link topic="issigtool">Inno Setup Signature Tool</link>.</p>
+<p>The precise effect of this flag depends on whether it is combined with the <tt>external</tt> flag:</p>
+<ul>
+<li>When used without the <tt>external</tt> flag, the compiler will verify the source file while it is being compressed/stored into the resulting installer. If the verification fails, compilation will abort.</li>
+<li><p>When used with the <tt>external</tt> flag, Setup will verify the source file during the installation process while it is being copied to the destination directory. Files are always created with temporary names (<tt>*.tmp</tt>) initially. If the verification fails, the temporary file will be deleted and a &quot;The source file is corrupted&quot; error message will be displayed to the user (with Skip, Try Again, and Cancel options) and a more detailed error is logged. If the verification succeeds, the temporary file will be renamed to the correct destination name.</p>
+<p>When a file entry with the <tt>external</tt> flag is skipped (i.e., not installed - for example because the <tt>ignoreversion</tt> flag wasn't used), the source file isn't copied anywhere, so no verification takes place.</p></li>
+</ul>
+<p>Since verification occurs while source files are being compressed/copied, and not in a separate pass, each file's contents are only read once. Thus, enabling verification has little performance impact; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. This approach also ensures there is no Time-Of-Check to Time-Of-Use (TOCTOU) problem; each source file is kept open the entire time it is being compressed/copied and verified without allowing other processes write access.</p>
+<p>This flag cannot be combined with the <tt>sign</tt> or <tt>signonce</tt> flags. Use <tt>signcheck</tt> instead.</p>
+</flag>
 <flag name="isreadme">
 <p>File is the "README" file. Only <i>one</i> file in an installation can have this flag. When a file has this flag, the user will asked if they would like to view the README file after the installation has completed. If Yes is chosen, Setup will open the file, using the default program for the file type. For this reason, the README file should always end with an extension like .txt, .wri, or .doc.</p>
 <p>Note that if Setup has to restart the user's computer (as a result of installing a file with the flag <tt>restartreplace</tt> or if the <tt>AlwaysRestart</tt> <tt>[Setup]</tt> section directive is <tt>yes</tt>), the user will not be given an option to view the README file.</p>
@@ -1763,12 +1785,14 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 </flag>
 <flag name="sign">
 <p>This flag instructs the compiler to digitally sign the original source files before storing them. Ignored if [Setup] section directive <link topic="setup_signtool">SignTool</link> is not set.</p>
+<p>This flag cannot be combined with the <tt>issigverify</tt> flag. Use <tt>signcheck</tt> instead.</p>
 </flag>
 <flag name="signcheck">
 <p>This flag instructs the compiler to check the original source files for a digital signature before storing them.</p>
 </flag>
 <flag name="signonce">
 <p>This flag instructs the compiler to digitally sign the original source files before storing them, but only if the files are not already signed. Ignored if [Setup] section directive <link topic="setup_signtool">SignTool</link> is not set.</p>
+<p>This flag cannot be combined with the <tt>issigverify</tt> flag. Use <tt>signcheck</tt> instead.</p>
 </flag>
 <flag name="skipifsourcedoesntexist">
 <p>This flag instructs the compiler -- or Setup, if the <tt>external</tt> flag is also used -- to silently skip over the entry if the source file does not exist, instead of displaying an error message.</p>
@@ -2756,6 +2780,81 @@ Type: files; Name: "{win}\MYPROG.INI"
 
 
 
+<topic name="issigkeyssection" title="[ISSigKeys] section">
+<keyword value="[ISSigKeys] section" />
+<keyword value="ISSigKeys" />
+<body>
+
+<p>This optional section defines any keys the compiler and Setup can use to verify files using <link topic="filessection">[Files]</link> section's <tt>issigverify</tt> flag.</p>
+
+<p>Here is an example of an <tt>[ISSigKeys]</tt> section:</p>
+
+<precode>
+[ISSigKeys]
+Name: "MyKey1; \
+  KeyID:   "def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38"; \
+  KeyFile: "MyKey1.ispublickey"
+Name: "MyKey2; \
+  KeyID:   "def020edee3c4835fd54d85eff8b66d4d899b22a777353ca4a114b652e5e7a28"; \
+  PublicX: "515dc7d6c16d4a46272ceb3d158c5630a96466ab4d948e72c2029d737c823097"; \
+  PublicY: "f3c21f6b5156c52a35f6f28016ee3e31a3ded60c325b81fb7b1f88c221081a61"
+</precode>
+
+<p>See the <i>Remarks</i> section at the bottom of this topic for some important notes.</p>
+
+<p>The following is a list of the supported <link topic="params">parameters</link>:</p>
+
+<paramlist>
+
+<param name="Name" required="yes">
+<p>The internal name of the key.</p>
+<p>Key names are not stored in the resulting Setup file(s), so you may use personal or non-public information in your key names, like the names of developers.</p>
+<example><pre>Name: "MyKey"</pre></example>
+</param>
+
+<param name="Group">
+<p>A space separated list of internal group names, specifying how to group the key.</p>
+<p>Group names are not stored in the resulting Setup file(s), so you may use personal or non-public information in your key group names, like the names of developer groups.</p>
+<example><pre>Group: "exesigner docsigner"</pre></example>
+</param>
+
+<param name="KeyID">
+<p>Specifies the ID of the key. If specified, the compiler uses it to double check the values of parameters <tt>KeyFile</tt>, <tt>PublicX</tt>, and <tt>PublicY</tt>. Is not used for anything else.</p>
+<example><pre>KeyID: "def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38"</pre></example>
+</param>
+
+<param name="KeyFile">
+<p>Specifies the private or public key file. The compiler will prepend the path of your installation's <link topic="sourcedirectorynotes">source directory</link> if you do not specify a fully qualified pathname.</p>
+<p>Must be set if parameters <tt>PublicX</tt> and <tt>PublicY</tt> aren't set.</p>
+<p>If a private key file is specified, only it's public information is stored in the resulting Setup file(s).</p>
+<example><pre>KeyFile: "MyKey.ispublickey"</pre></example>
+</param>
+
+<param name="PublicX">
+<p>Specifies the "public-x" value of the key.</p>
+<p>Must be set if parameter <tt>KeyFile</tt> isn't set.</p>
+<example><pre>PublicX: "e3e943066aff8f28d2219fd71c9ffff4c8d1aa26bc4225434be67180ab5e242d"</pre></example>
+</param>
+
+<param name="PublicY">
+<p>Specifies the "public-y" value of the key.</p>
+<p>Must be set if parameter <tt>KeyFile</tt> isn't set.</p>
+<example><pre>PublicX: "e419041c3f54551e86a1c47f387005cd535dfc9d64339b30d37f9a4f7866b650"</pre></example>
+</param>
+
+</paramlist>
+
+<heading>Remarks</heading>
+
+<p>Keys and key files should be created using <link topic="issigtool">Inno Setup Signature Tool</link>.</p>
+
+<p>Created key files are human readable and can be opened with any text editor to get a key's <tt>KeyID</tt>, <tt>PublicX</tt>, and <tt>PublicY</tt> values. Note that none of these values are required if you set the <tt>KeyFile</tt> parameter instead.</p>
+
+</body>
+</topic>
+
+
+
 <topic name="examples" title="Example Scripts">
 <keyword value="Example Scripts" />
 <body>
@@ -3493,13 +3592,138 @@ Filename: "{win}\MYPROG.INI"; Section: "InstallSettings"; Key: "InstallPath"; St
 
 
 
+<topic name="issig" title=".issig Signatures: Introduction">
+<body>
+
+<p>Inno Setup includes an integrated signature-verification capability that can be used to detect corruption or tampering in files at compile time, before files are included in an installer being built, or during installation, before Setup copies external files onto a user's system.</p>
+
+<p>Signatures are created using the included <link topic="issigtool">Inno Setup Signature Tool</link> utility (<tt>ISSigTool.exe</tt>) and are stored in separate files with an <tt>.issig</tt> extension. Because the signatures are stored in separate files — the original files are not touched — any type of file may be signed and verified.</p>
+
+<p>Creation of <tt>.issig</tt> signatures does <i>not</i> require a certificate from a certificate authority. There is no cost involved.</p>
+
+<p>Note, however, that an <tt>.issig</tt> signature cannot be used to eliminate an "Unknown publisher" warning message shown by Windows when an installer or other EXE file is started. That requires a completely different kind of signature (Authenticode) embedded inside the EXE file by a different tool (Microsoft's <tt>signtool.exe</tt>), and it does require a (usually expensive) code-signing certificate from a certificate authority. You can however use both <tt>signtool.exe</tt> and <tt>ISSigTool.exe</tt> on a single file, in that order. If you are looking for more information about <tt>signtool.exe</tt> see <link topic="setup_signtool">SignTool</link> instead.</p>
+
+<heading>Quick start: Verifying files at compile time</heading>
+
+<p>On the <tt>issigtool</tt> commands below, prepend the path of your Inno Setup installation, and include quotes. For example: <tt>"%ProgramFiles(x86)%\Inno Setup 6\issigtool"</tt></p>
+
+<ol>
+
+<li>
+<p>At the command line, generate a new private key file that will be used for signing:</p>
+<precode>
+issigtool --key-file="MyKey.isprivatekey" generate-private-key
+</precode>
+<p>A file named <tt>MyKey.isprivatekey</tt> will be created in the current directory. You may include a pathname if you wish.</p>
+<p>The file should not be shared with others, and should never be committed into a public repository. To reduce the risk of accidental disclosure, it is best to keep the file outside of your source tree.</p>
+</li>
+
+<li>
+<p>Create the file we want to sign, then create the signature:</p>
+<precode>
+echo Hello &gt; MyFile.txt
+issigtool --key-file="MyKey.isprivatekey" sign "MyFile.txt"
+</precode>
+<p>A signature file named <tt>MyFile.txt.issig</tt> is created.</p>
+</li>
+
+<li>
+<p>Inside your Inno Setup script, add an <link topic="issigkeyssection">[ISSigKeys] section</link> entry linking to your key file, and a <link topic="filessection">[Files] section</link> entry for <tt>MyFile.txt</tt> that includes the <tt>issigverify</tt> flag:</p>
+<precode>
+[ISSigKeys]
+Name: MyKey; KeyFile: "MyKey.isprivatekey"
+
+[Files]
+Source: "MyFile.txt"; DestDir: "{app}"; Flags: issigverify
+</precode>
+<p>Note: Specifying a <i>public</i> key file instead is preferred; see the <link topic="issig" anchor="tips">Tips &amp; Recommendations</link> section below.</p>
+</li>
+
+<li>
+<p>Compile the script. In the compiler output, you should see a line indicating the file was successfully verified:</p>
+<precode>
+   Compressing: MyFile.txt
+      ISSig verification successful.
+</precode>
+</li>
+
+<li>
+<p>Now let's confirm that the compiler actually does catch corruption or tampering within the file.</p>
+<p>Make a modification to <tt>MyFile.txt</tt> — for example, add or change a character.</p>
+</li>
+
+<li>
+<p>Compile the script again. This time, you should get an error like the following:</p>
+<precode>
+Signature is not valid for source file "MyFile.txt": file hash incorrect.
+</precode>
+</li>
+
+</ol>
+
+<heading>Verifying external files during installation</heading>
+
+<p>The procedure for setting up verification of <tt>external</tt> files is essentially the same as the procedure shown above for compile-time verification, except:</p>
+
+<ul>
+
+<li>
+<p>The <tt>[Files]</tt> section entry would include a path in the <tt>Source</tt> parameter, and include the <tt>external</tt> flag:</p>
+<precode>
+[Files]
+Source: "{src}\MyFile.txt"; DestDir: "{app}"; Flags: external issigverify
+</precode>
+</li>
+
+<li>
+<p>The signature file — <tt>MyFile.txt.issig</tt> in this example — must exist in the same directory as the <tt>Source</tt> file during the installation process. (No compile-time verification occurs on <tt>external</tt> files.)</p>
+</li>
+
+</ul>
+
+<heading><a name="tips">Tips &amp; Recommendations</a></heading>
+
+<ul>
+
+<li>
+<p>Although an <link topic="issigkeyssection">[ISSigKeys] section</link> entry's <tt>KeyFile</tt> parameter can point to a private key file as demonstrated above, it is recommended that a <i>public</i> key file be specified instead whenever possible. Unlike a private key file, a public key file does not have to be kept secret, and <i>can</i> be safely committed into a version control repository.</p>
+<p>To create a public key file (<tt>MyKey.ispublickey</tt>) from an existing private key file (<tt>MyKey.isprivatekey</tt>), run this command:</p>
+<precode>
+issigtool --key-file="MyKey.isprivatekey" export-public-key "MyKey.ispublickey"
+</precode>
+<p>Alternatively, the public key values may be specified directly inside your script by using the <tt>PublicX</tt> and <tt>PublicY</tt> parameters instead of <tt>KeyFile</tt>.</p>
+</li>
+
+<li>
+<p>To avoid having to repeat the same <tt>--key-file=</tt> parameter on every <tt>issigtool</tt> command invocation, you may instead set the <tt>ISSIGTOOL_KEY_FILE</tt> environment variable to the path of your key file.</p>
+<p>In <tt>cmd.exe</tt> or a batch file:</p>
+<precode>
+set ISSIGTOOL_KEY_FILE=MyKey.isprivatekey
+</precode>
+<p>In PowerShell:</p>
+<precode>
+$env:ISSIGTOOL_KEY_FILE = "MyKey.isprivatekey"
+</precode>
+<p>The above variable assignments are non-persistent; they only affect the current <tt>cmd.exe</tt> or PowerShell session.</p>
+<p>Afterward, you may simply run:</p>
+<precode>
+issigtool sign "MyFile.txt"
+issigtool verify "MyFile.txt"
+</precode>
+</li>
+
+</ul>
+
+</body>
+</topic>
+
+
+
 <topic name="issigtool" title="Inno Setup Signature Tool">
 <keyword value="ISSigTool" />
 <body>
 
-<p>Inno Setup includes a command-line tool, ISSigTool.exe. This tool is designed to sign files using ECDSA P-256 cryptographic signatures.</p>
-
-<p>Note: ISSigTool.exe does not replace Microsoft's signtool.exe in any way and is in fact not related to Authenticode Code Signing at all. If you are looking for more information about this topic see <link topic="setup_signtool">SignTool</link> instead.</p>
+<p>Inno Setup includes a command-line utility, <tt>ISSigTool.exe</tt>. This utility is designed to sign files using ECDSA P-256 cryptographic <link topic="issig">signatures</link>.</p>
 
 <p>Command line usage is as follows:</p>
 
@@ -3553,10 +3777,10 @@ Filename: "{win}\MYPROG.INI"; Section: "InstallSettings"; Key: "InstallPath"; St
 
 <indent>
 <examples noheader="1">
-issigtool --key-file=MyKey.isprivatekey generate-private-key<br/>
-issigtool --key-file=MyKey.isprivatekey sign MyProg.dll<br/>
-issigtool --key-file=MyKey.isprivatekey export-public-key MyKey.ispublickey<br/>
-issigtool --key-file=MyKey.ispublickey verify MyProg.dll
+issigtool --key-file="MyKey.isprivatekey" generate-private-key<br/>
+issigtool --key-file="MyKey.isprivatekey" sign "MyProg.dll"<br/>
+issigtool --key-file="MyKey.isprivatekey" export-public-key "MyKey.ispublickey"<br/>
+issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"
 </examples>
 </indent>
 
@@ -5237,7 +5461,7 @@ DiskSliceSize=1457664
 <setupdefault><tt>Output</tt></setupdefault>
 <body>
 <p>Specifies the "output" directory for the script, which is where the compiler will place the resulting SETUP.* files. By default, it creates a directory named "Output" under the directory containing the script for this.</p>
-<p>If <tt>OutputDir</tt> is not a fully-qualified pathname, it will be treated as being relative to <tt>SourceDir</tt>, unless the pathname is prefixed by "userdocs:", in which case it will be treated as being relative to the the My Documents folder of the currently logged-in user. Setting <tt>OutputDir</tt> to <tt>.</tt> will result in the files being placed in the source directory.</p>
+<p>If <tt>OutputDir</tt> is not a fully-qualified pathname, it will be treated as being relative to <tt>SourceDir</tt>, unless the pathname is prefixed by "userdocs:", in which case it will be treated as being relative to the My Documents folder of the currently logged-in user. Setting <tt>OutputDir</tt> to <tt>.</tt> will result in the files being placed in the source directory.</p>
 <example><pre>OutputDir=c:\output</pre></example>
 </body>
 </setuptopic>
@@ -5698,7 +5922,7 @@ ArchitecturesInstallIn64BitMode=x64compatible
 <setupdefault><tt>yes</tt> if a <link topic="setup_signtool">SignTool</link> is set, <tt>no</tt> otherwise</setupdefault>
 <body>
 <p>Specifies whether the uninstaller program (unins???.exe) should be deployed with a digital signature attached. When the uninstaller has a valid digital signature, users will not see an "unknown publisher" warning when launching it.</p>
-<p>The first time you compile a script with this directive set to <tt>yes</tt>, a uniquely-named copy of the uninstaller EXE file will be created in the directory specified by the <link topic="setup_signeduninstallerdir">SignedUninstallerDir</link> directive (which defaults to the <link topic="setup_outputdir">output directory</link>). Depending on the <link topic="setup_signtool">SignTool</link> setting, you will either then be prompted to attach a digital signature to this file using an external code-signing tool (such as Microsoft's signtool.exe) or the file will be automatically signed on the fly. On subsequent compiles, the signature from the file will be embedded into the compiled installations' uninstallers.</p>
+<p>The first time you compile a script with this directive set to <tt>yes</tt>, a uniquely-named copy of the uninstaller EXE file will be created in the directory specified by the <link topic="setup_signeduninstallerdir">SignedUninstallerDir</link> directive (which defaults to the <link topic="setup_outputdir">output directory</link>). Depending on the <link topic="setup_signtool">SignTool</link> setting, you will either then be prompted to attach a digital signature to this file using an external code-signing tool (such as Microsoft's <tt>signtool.exe</tt>) or the file will be automatically signed on the fly. On subsequent compiles, the signature from the file will be embedded into the compiled installations' uninstallers.</p>
 <p>Upgrading to a newer version of Inno Setup, or changing certain [Setup] section directives that affect the contents of the uninstaller EXE file (such as <link topic="setup_setupiconfile">SetupIconFile</link> and VersionInfo directives), will cause a new file to be created under a different name.</p>
 <p>If a file generated by this directive is deleted, it will be recreated automatically if necessary on the next compile.</p>
 <p>When the uninstaller has a digital signature, Setup will write the messages from the active language into a separate file (unins???.msg). It cannot embed the messages into the EXE file because doing so would invalidate the digital signature.</p>

+ 5 - 6
ISHelp/isxfunc.xml

@@ -1892,21 +1892,20 @@ end;</pre>
 <p><tt>TOnExtractionProgress = function(const ArchiveName, FileName: String; const Progress, ProgressMax: Int64): Boolean;</tt></p>
 <p>Return True to allow the extraction to continue, False otherwise.</p>
 <p><tt>Extract7ZipArchive</tt> uses an embedded version of the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements.</p>
-<p>All output of the decoder is logged if logging is enabled, including error messages but excluding empty lines.</p>
-<p>The decoder has the following limitations, as written by Igor Pavlov in the LZMA SDK:</p>
+<p>All output of the decoder is logged if logging is enabled, including error messages but excluding empty lines.</p></remarks>
+        <limitations><p>The decoder has the following limitations, as written by Igor Pavlov in the LZMA SDK:</p>
 <ul>
 <li>It reads only &quot;FileName&quot;, &quot;Size&quot;, &quot;LastWriteTime&quot; and &quot;CRC&quot; information for each file in archive.</li>
 <li>It does not support PPMd and BZip2 methods.</li>
 <li>It converts original UTF-16 Unicode file names to UTF-8 Unicode file names.</li>
 <li>It decodes whole solid block from 7z archive to RAM. The RAM consumption can be high.</li>
 </ul>
-<p>To expand on his comments about RAM consumption: When extracting a file, at least enough memory will always be allocated to hold the entire file, regardless of the block size. For example, extracting a 1 GB file using <tt>Extract7ZipArchive</tt> requires at least 1 GB of RAM. Consider using a different solution for extracting large files, such as embedding 7-Zip itself, which does not use as much RAM, into your installation.</p>
+<p>To expand on his comments about RAM consumption: When extracting a file, at least enough memory will always be allocated to hold the <b>entire</b> file, regardless of the block size. For example, extracting a 1 GB file using <tt>Extract7ZipArchive</tt> requires at least 1 GB of RAM. Consider using a different solution for extracting large files, such as embedding 7-Zip itself, which does not use as much RAM, into your installation.</p>
 <p>Additionally he wrote:</p>
 <ul>
 <li>You can create .7z archive with 7z.exe, 7za.exe or 7zr.exe:<br />
 <tt>7z.exe a archive.7z *.htm -r -mx -m0fb=255</tt></li>
-</ul>
-</remarks>
+</ul></limitations>
         <seealso><p><link topic="isxfunc_CreateExtractionPage">CreateExtractionPage</link><br />
 <link topic="isxfunc_CreateDownloadPage">CreateDownloadPage</link><br />
 <link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link><br />
@@ -2682,7 +2681,7 @@ Page := CreateOutputMsgMemoPage(wpWelcome,
 <p>To start the extraction, call the <tt>Extract</tt> method. An exception will be raised if there was an error. Otherwise, <tt>Extract</tt> returns the number of archives extracted.</p>
 <p>Set the <tt>ShowArchiveInsteadFile</tt> property to <tt>True</tt> to show the name of the archive which is being extracted to the user instead of the names of the files inside the archive.</p>
 <p>See <link topic="isxfunc_Extract7ZipArchive">Extract7ZipArchive</link> for other considerations and the definition of <tt>TOnExtractionProgress</tt>.</p></remarks>
-        <example><p>See <i>CodeDownloadFiles.iss</i> for an example of <tt>CreateDownloadPage</tt> which works very similar to <tt>CreateExtractionPage</tt>.</p></example>
+        <example><p>See <i>CodeDownloadFiles.iss</i> for an example.</p></example>
         <seealso><p><link topic="scriptclasses" anchor="TExtractionWizardPage">TExtractionWizardPage</link><br />
 <link topic="isxfunc_Extract7ZipArchive">Extract7ZipArchive</link><br />
 <link topic="isxfunc_CreateOutputProgressPage">CreateOutputProgressPage</link></p></seealso>

+ 7 - 0
ISHelp/isxfunc.xsl

@@ -107,6 +107,13 @@
 </xsl:when>
 </xsl:choose>
 
+<xsl:choose>
+<xsl:when test="limitations">
+<p margin="no"><b>Limitations:</b></p>
+<xsl:apply-templates select="limitations"/>
+</xsl:when>
+</xsl:choose>
+
 <xsl:choose>
 <xsl:when test="example">
 <p margin="no"><b>Example:</b></p>

+ 4 - 1
Projects/Setup.dpr

@@ -95,7 +95,10 @@ uses
   PBKDF2 in '..\Components\PBKDF2.pas',
   Compression.SevenZipDecoder in 'Src\Compression.SevenZipDecoder.pas',
   PSStackHelper in '..\Components\PSStackHelper.pas',
-  Setup.ScriptFunc.HelperFunc in 'Src\Setup.ScriptFunc.HelperFunc.pas';
+  Setup.ScriptFunc.HelperFunc in 'Src\Setup.ScriptFunc.HelperFunc.pas',
+  ECDSA in '..\Components\ECDSA.pas',
+  ISSigFunc in '..\Components\ISSigFunc.pas',
+  StringScanner in '..\Components\StringScanner.pas';
 
 {$SETPEOSVERSION 6.1}
 {$SETPESUBSYSVERSION 6.1}

+ 3 - 0
Projects/Setup.dproj

@@ -168,6 +168,9 @@
         <DCCReference Include="Src\Compression.SevenZipDecoder.pas"/>
         <DCCReference Include="..\Components\PSStackHelper.pas"/>
         <DCCReference Include="Src\Setup.ScriptFunc.HelperFunc.pas"/>
+        <DCCReference Include="..\Components\ECDSA.pas"/>
+        <DCCReference Include="..\Components\ISSigFunc.pas"/>
+        <DCCReference Include="..\Components\StringScanner.pas"/>
         <BuildConfiguration Include="Base">
             <Key>Base</Key>
         </BuildConfiguration>

+ 24 - 4
Projects/Src/Compiler.Messages.pas

@@ -53,6 +53,7 @@ const
   SCompilerStatusFilesCompressingVersion = '   Compressing: %s   (%u.%u.%u.%u)';
   SCompilerStatusFilesStoring = '   Storing: %s';
   SCompilerStatusFilesStoringVersion = '   Storing: %s   (%u.%u.%u.%u)';
+  SCompilerStatusFilesISSigVerified = '      ISSig verification successful.';
   SCompilerStatusCompressingSetupExe = '   Compressing Setup program executable';
   SCompilerStatusUpdatingVersionInfo = '   Updating version info (%s)';
   SCompilerStatusUpdatingManifest = '   Updating manifest (%s)';
@@ -84,9 +85,16 @@ const
   SCompilerUnknownFilenamePrefix = 'Unknown filename prefix "%s"';
   SCompilerSourceFileDoesntExist = 'Source file "%s" does not exist';
   SCompilerSourceFileNotSigned = 'Source file "%s" is not signed';
+  SCompilerSourceFileISSigMissingFile = 'Signature file does not exist for source file "%s"';
+  SCompilerSourceFileISSigInvalidSignature = 'Signature is not valid for source file "%s": %s';
+  SCompilerSourceFileISSigMalformedOrBadSignature = 'malformed or bad signature';
+  SCompilerSourceFileISSigKeyNotFound = 'no matching key found';
+  SCompilerSourceFileISSigUnknownVerifyResult = 'unknown verify result';
+  SCompilerSourceFileISSigFileSizeIncorrect = 'file size incorrect';
+  SCompilerSourceFileISSigFileHashIncorrect = 'file hash incorrect';
   SCompilerCopyError3 = 'Could not copy "%s" to "%s".' + SNewLine2 + 'Error %d: %s';
   SCompilerReadError = 'Could not read "%s".' + SNewLine2 + 'Error: %s';
-  SCompilerCompressError2 = 'An internal error occurred while trying to compress "%s"';
+  SCompilerCompressInternalError = 'An internal error occurred during compression: %s';
   SCompilerNotEnoughSpaceOnFirstDisk = 'There is not enough space on the first disk to copy all of the required files';
   SCompilerSetup0Mismatch = 'Internal error SC1';
   SCompilerMustUseDiskSpanning = 'Disk spanning must be enabled in order to create an installation larger than %d bytes in size';
@@ -195,12 +203,14 @@ const
   SCompilerParamDataTooLong = 'Data on parameter "%s" is too long';
   SCompilerParamUnknownParam = 'Unrecognized parameter name "%s"';
   SCompilerParamDuplicated = 'Cannot have multiple "%s" parameters';
+  SCompilerParamConflict = 'Cannot have both the "%s" and "%s" parameters';
   SCompilerParamEmpty2 = 'Parameter "%s" is empty';
   SCompilerParamNotSpecified = 'Required parameter "%s" not specified';
   SCompilerParamNoQuotes2 = 'Parameter "%s" cannot include quotes (")';
   SCompilerParamNoBackslash = 'Parameter "%s" cannot include backslashes (\)';
   SCompilerParamNoPrecedingBackslash = 'Parameter "%s" cannot begin with a backslash (\)';
   SCompilerParamInvalid2 = 'Parameter "%s" is not a valid value';
+  SCompilerParamInvalidWithError = 'Parameter "%s" is not a valid value: %s';
 
   { Flags }
   SCompilerParamUnknownFlag2 = 'Parameter "%s" includes an unknown flag';
@@ -234,12 +244,18 @@ const
   { [Types] }
   SCompilerTypesCustomTypeAlreadyDefined = 'A custom type has already been defined';
 
-  { [Components], [Tasks], [Languages] }
+  { [Components], [Tasks], [Languages], [ISSigKeys] }
   SCompilerComponentsOrTasksBadName = 'Parameter "Name" includes invalid characters.' + SNewLine2 +
     'It may only include alphanumeric characters, underscores, slashes (/), and/or backslashes (\), may not start with a number and may not start or end with a slash or a backslash. Names ''not'', ''and'' and ''or'' are reserved';
   SCompilerComponentsInvalidLevel = 'Component cannot be more than one level below the preceding component';
-  SCompilerTasksInvalidLevel = 'Task cannot be more than one level below the preceding task'; 
-  SCompilerLanguagesBadName = 'Parameter "Name" includes invalid characters.' + SNewLine2 + 'It may only include alphanumeric characters and/or underscores, and may not start with a number. Names ''not'', ''and'' and ''or'' are reserved';
+  SCompilerTasksInvalidLevel = 'Task cannot be more than one level below the preceding task';
+  SCompilerLanguagesOrISSigKeysBadName = 'Parameter "%s" includes invalid characters.' + SNewLine2 + 'It may only include alphanumeric characters and/or underscores, and may not start with a number. Names ''not'', ''and'' and ''or'' are reserved';
+  SCompilerLanguagesOrISSigKeysBadGroupName = 'Parameter "%s" includes a name with invalid characters.' + SNewLine2 + 'Names may only include alphanumeric characters and/or underscores, and may not start with a number. Names ''not'', ''and'' and ''or'' are reserved';
+  SCompilerISSigKeysNameExists = 'Name "%s" is already in use"';
+  SCompilerISSigKeysKeyNotSpecified = 'Required parameter(s) "KeyFile" or "PublicX"/"PublicY" not specified';
+  SCompilerISSigKeysBadKeyID = 'Value of parameter "KeyID" is not valid for given "KeyFile" or "PublicX"/"PublicY" values.';
+  SCompilerISSigKeysBadKeyFile = 'Key file is malformed';
+  SCompilerISSigKeysUnknownKeyImportResult = 'Unknown import key result';
 
   { [Languages] }
   SCompilerParamUnknownLanguage = 'Parameter "%s" includes an unknown language';
@@ -292,6 +308,10 @@ const
   SCompilerFilesWarningSharedFileSysWow64 = 'DestDir should not be set to ' +
     '"{syswow64}" when the "sharedfile" flag is used. See the "sharedfile" ' +
     'documentation in the help file for details.';
+  SCompilerFilesISSigVerifyMissingISSigKeys = 'Flag "issigverify" may not be used when the "ISSigKeys" section doesn''t exist or is empty.';
+  SCompilerFilesISSigAllowedKeysMissingISSigVerify = 'Flag "issigverify" must be used when the "ISSigAllowedKeys" parameter is used.';
+  SCompilerFilesISSigAllowedKeysConflict = 'Parameter "ISSigAllowedKeys" cannot allow different keys on the same source file';
+  SCompilerFilesUnkownISSigKeyNameOrGroupName = 'Parameter "%s" includes an unknown name or group name.';
 
   { [Icons] }
   SCompilerIconsNamePathNotSpecified = 'Parameter "Name" must include a path for the icon, ' +

+ 273 - 14
Projects/Src/Compiler.SetupCompiler.pas

@@ -76,6 +76,7 @@ type
     ComponentEntries,
     TaskEntries,
     DirEntries,
+    ISSigKeyEntries,
     FileEntries,
     FileLocationEntries,
     IconEntries,
@@ -88,6 +89,7 @@ type
 
     FileLocationEntryFilenames: THashStringList;
     FileLocationEntryExtraInfos: TList;
+    ISSigKeyEntryExtraInfos: TList;
     WarningsList: THashStringList;
     ExpectedCustomMessageNames: TStringList;
     MissingMessagesWarning, MissingRunOnceIdsWarning, MissingRunOnceIds, NotRecognizedMessagesWarning, UsedUserAreasWarning: Boolean;
@@ -190,6 +192,7 @@ type
     procedure EnumLanguagesProc(const Line: PChar; const Ext: Integer);
     procedure EnumRegistryProc(const Line: PChar; const Ext: Integer);
     procedure EnumDeleteProc(const Line: PChar; const Ext: Integer);
+    procedure EnumISSigKeysProc(const Line: PChar; const Ext: Integer);
     procedure EnumFilesProc(const Line: PChar; const Ext: Integer);
     procedure EnumRunProc(const Line: PChar; const Ext: Integer);
     procedure EnumSetupProc(const Line: PChar; const Ext: Integer);
@@ -289,7 +292,7 @@ implementation
 
 uses
   Commctrl, TypInfo, AnsiStrings, Math, WideStrUtils,
-  PathFunc, TrustFunc, Shared.CommonFunc, Compiler.Messages, Shared.SetupEntFunc,
+  PathFunc, TrustFunc, ISSigFunc, ECDSA, Shared.CommonFunc, Compiler.Messages, Shared.SetupEntFunc,
   Shared.FileClass, Compression.Base, Compression.Zlib, Compression.bzlib,
   Shared.LangOptionsSectionDirectives, Shared.ResUpdateFunc, Compiler.ExeUpdateFunc,
 {$IFDEF STATICPREPROC}
@@ -308,12 +311,21 @@ type
     Name, Command: String;
   end;
 
+  PISSigKeyEntryExtraInfo = ^TISSigKeyEntryExtraInfo;
+  TISSigKeyEntryExtraInfo = record
+    Name: String;
+    GroupNames: array of String;
+    function HasGroupName(const GroupName: String): Boolean;
+  end;
+
   TFileLocationSign = (fsNoSetting, fsYes, fsOnce, fsCheck);
   PFileLocationEntryExtraInfo = ^TFileLocationEntryExtraInfo;
   TFileLocationEntryExtraInfo = record
     Flags: set of (floVersionInfoNotValid, floIsUninstExe, floApplyTouchDateTime,
-      floSolidBreak);
+      floSolidBreak, floISSigVerify);
     Sign: TFileLocationSign;
+    ISSigAllowedKeys: AnsiString;
+    ISSigKeyUsedID: String;
   end;
 
 var
@@ -348,6 +360,16 @@ begin
   until (Result <> '') or (S = '');
 end;
 
+{ TISSigKeyEntryExtraInfo }
+
+function TISSigKeyEntryExtraInfo.HasGroupName(const GroupName: String): Boolean;
+begin
+  for var I := 0 to Length(GroupNames)-1 do
+    if SameText(GroupNames[I], GroupName) then
+      Exit(True);
+  Result := False;
+end;
+
 { TSetupCompiler }
 
 constructor TSetupCompiler.Create(AOwner: TComponent);
@@ -361,6 +383,7 @@ begin
   ComponentEntries := TList.Create;
   TaskEntries := TList.Create;
   DirEntries := TList.Create;
+  ISSigKeyEntries := TList.Create;
   FileEntries := TList.Create;
   FileLocationEntries := TList.Create;
   IconEntries := TList.Create;
@@ -372,6 +395,7 @@ begin
   UninstallRunEntries := TList.Create;
   FileLocationEntryFilenames := THashStringList.Create;
   FileLocationEntryExtraInfos := TList.Create;
+  ISSIgKeyEntryExtraInfos := TList.Create;
   WarningsList := THashStringList.Create;
   WarningsList.IgnoreDuplicates := True;
   ExpectedCustomMessageNames := TStringList.Create;
@@ -419,6 +443,7 @@ begin
   UsedUserAreas.Free;
   ExpectedCustomMessageNames.Free;
   WarningsList.Free;
+  ISSigKeyEntryExtraInfos.Free;
   FileLocationEntryExtraInfos.Free;
   FileLocationEntryFilenames.Free;
   UninstallRunEntries.Free;
@@ -430,6 +455,7 @@ begin
   IconEntries.Free;
   FileLocationEntries.Free;
   FileEntries.Free;
+  ISSigKeyEntries.Free;
   DirEntries.Free;
   TaskEntries.Free;
   ComponentEntries.Free;
@@ -4462,6 +4488,132 @@ begin
   end;
 end;
 
+procedure TSetupCompiler.EnumISSigKeysProc(const Line: PChar; const Ext: Integer);
+
+  function ISSigKeysNameExists(const Name: String; const CheckGroupNames: Boolean): Boolean;
+  begin
+    for var I := 0 to ISSigKeyEntryExtraInfos.Count-1 do begin
+      var ISSigKeyEntryExtraInfo := PISSigKeyEntryExtraInfo(ISSigKeyEntryExtraInfos[I]);
+      if SameText(ISSigKeyEntryExtraInfo.Name, Name) or
+         (CheckGroupNames and ISSigKeyEntryExtraInfo.HasGroupName(Name)) then
+        Exit(True)
+    end;
+    Result := False;
+  end;
+
+type
+  TParam = (paName, paGroup, paKeyFile, paKeyID, paPublicX, paPublicY);
+const
+  ParamISSigKeysName = 'Name';
+  ParamISSigKeysGroup = 'Group';
+  ParamISSigKeysKeyFile = 'KeyFile';
+  ParamISSigKeysKeyID = 'KeyID';
+  ParamISSigKeysPublicX = 'PublicX';
+  ParamISSigKeysPublicY = 'PublicY';
+  ParamInfo: array[TParam] of TParamInfo = (
+    (Name: ParamISSigKeysName; Flags: [piRequired, piNoEmpty]),
+    (Name: ParamISSigKeysGroup; Flags: []),
+    (Name: ParamISSigKeysKeyFile; Flags: [piNoEmpty]),
+    (Name: ParamISSigKeysKeyID; Flags: [piNoEmpty]),
+    (Name: ParamISSigKeysPublicX; Flags: [piNoEmpty]),
+    (Name: ParamISSigKeysPublicY; Flags: [piNoEmpty]));
+var
+  Values: array[TParam] of TParamValue;
+  NewISSigKeyEntry: PSetupISSigKeyEntry;
+  NewISSigKeyEntryExtraInfo: PISSigKeyEntryExtraInfo;
+begin
+  ExtractParameters(Line, ParamInfo, Values);
+
+  NewISSigKeyEntry := nil;
+  NewISSigKeyEntryExtraInfo := nil;
+  try
+    NewISSigKeyEntryExtraInfo := AllocMem(SizeOf(TISSigKeyEntryExtraInfo));
+    with NewISSigKeyEntryExtraInfo^ do begin
+      { Name }
+      Name := Values[paName].Data;
+      if not IsValidIdentString(Name, False, False) then
+        AbortCompileFmt(SCompilerLanguagesOrISSigKeysBadName, [ParamISSigKeysName])
+      else if ISSigKeysNameExists(Name, True) then
+        AbortCompileFmt(SCompilerISSigKeysNameExists, [Name]);
+
+      { Group }
+      var S := Values[paGroup].Data;
+      while True do begin
+        const GroupName = ExtractStr(S, ' ');
+        if GroupName = '' then
+          Break;
+        if not IsValidIdentString(GroupName, False, False) then
+          AbortCompileFmt(SCompilerLanguagesOrISSigKeysBadGroupName, [ParamISSigKeysGroup])
+        else if SameText(Name, GroupName) or ISSigKeysNameExists(GroupName, False) then
+          AbortCompileFmt(SCompilerISSigKeysNameExists, [GroupName]);
+        if not HasGroupName(GroupName) then begin
+          const N = Length(GroupNames);
+          SetLength(GroupNames, N+1);
+          GroupNames[N] := GroupName;
+        end;
+      end;
+    end;
+
+    NewISSigKeyEntry := AllocMem(SizeOf(TSetupISSigKeyEntry));
+    with NewISSigKeyEntry^ do begin
+      { KeyFile & PublicX & PublicY }
+      var KeyFile := PrependSourceDirName(Values[paKeyFile].Data);
+      PublicX := Values[paPublicX].Data;
+      PublicY := Values[paPublicY].Data;
+
+      if (KeyFile = '') and (PublicX = '') and (PublicY = '') then
+        AbortCompile(SCompilerISSigKeysKeyNotSpecified)
+      else if KeyFile <> '' then begin
+        if PublicX <> '' then
+          AbortCompileFmt(SCompilerParamConflict, [ParamISSigKeysKeyFile, ParamISSigKeysPublicX])
+        else if PublicY <> '' then
+          AbortCompileFmt(SCompilerParamConflict, [ParamISSigKeysKeyFile, ParamISSigKeysPublicY]);
+        var KeyText := ISSigLoadTextFromFile(KeyFile);
+        var PublicKey: TECDSAPublicKey;
+        const ParseResult = ISSigParsePublicKeyText(KeyText, PublicKey);
+        if ParseResult = ikrMalformed then
+          AbortCompile(SCompilerISSigKeysBadKeyFile)
+        else if ParseResult <> ikrSuccess then
+          AbortCompile(SCompilerISSigKeysUnknownKeyImportResult);
+        ISSigConvertPublicKeyToStrings(PublicKey, PublicX, PublicY);
+      end else begin
+        if PublicX = '' then
+          AbortCompileParamError(SCompilerParamNotSpecified, ParamISSigKeysPublicX)
+        else if PublicY = '' then
+          AbortCompileParamError(SCompilerParamNotSpecified, ParamISSigKeysPublicY);
+        try
+          ISSigCheckValidPublicXOrY(PublicX);
+        except
+          AbortCompileFmt(SCompilerParamInvalidWithError, [ParamISSigKeysPublicX, GetExceptMessage]);
+        end;
+        try
+          ISSigCheckValidPublicXOrY(PublicY);
+        except
+          AbortCompileFmt(SCompilerParamInvalidWithError, [ParamISSigKeysPublicY, GetExceptMessage]);
+        end;
+      end;
+
+      { KeyID }
+      var KeyID := Values[paKeyID].Data;
+      if KeyID <> '' then begin
+        try
+          ISSigCheckValidKeyID(KeyID);
+        except
+          AbortCompileFmt(SCompilerParamInvalidWithError, [ParamISSigKeysKeyID, GetExceptMessage]);
+        end;
+        if not ISSigIsValidKeyIDForPublicXY(KeyID, PublicX, PublicY) then
+          AbortCompile(SCompilerISSigKeysBadKeyID);
+      end;
+    end;
+  except
+    SEFreeRec(NewISSigKeyEntry, SetupISSigKeyEntryStrings, SetupISSigKeyEntryAnsiStrings);
+    Dispose(NewISSigKeyEntryExtraInfo);
+    raise;
+  end;
+  ISSigKeyEntries.Add(NewISSigKeyEntry);
+  ISSigKeyEntryExtraInfos.Add(NewISSigKeyEntryExtraInfo);
+end;
+
 procedure TSetupCompiler.EnumFilesProc(const Line: PChar; const Ext: Integer);
 
   function EscapeBraces(const S: String): String;
@@ -4483,8 +4635,8 @@ procedure TSetupCompiler.EnumFilesProc(const Line: PChar; const Ext: Integer);
 type
   TParam = (paFlags, paSource, paDestDir, paDestName, paCopyMode, paAttribs,
     paPermissions, paFontInstall, paExcludes, paExternalSize, paStrongAssemblyName,
-    paComponents, paTasks, paLanguages, paCheck, paBeforeInstall, paAfterInstall,
-    paMinVersion, paOnlyBelowVersion);
+    paISSigAllowedKeys, paComponents, paTasks, paLanguages, paCheck, paBeforeInstall,
+    paAfterInstall, paMinVersion, paOnlyBelowVersion);
 const
   ParamFilesSource = 'Source';
   ParamFilesDestDir = 'DestDir';
@@ -4496,6 +4648,7 @@ const
   ParamFilesExcludes = 'Excludes';
   ParamFilesExternalSize = 'ExternalSize';
   ParamFilesStrongAssemblyName = 'StrongAssemblyName';
+  ParamFilesISSigAllowedKeys = 'ISSigAllowedKeys';
   ParamInfo: array[TParam] of TParamInfo = (
     (Name: ParamCommonFlags; Flags: []),
     (Name: ParamFilesSource; Flags: [piRequired, piNoEmpty, piNoQuotes]),
@@ -4508,6 +4661,7 @@ const
     (Name: ParamFilesExcludes; Flags: []),
     (Name: ParamFilesExternalSize; Flags: []),
     (Name: ParamFilesStrongAssemblyName; Flags: [piNoEmpty]),
+    (Name: ParamFilesISSigAllowedKeys; Flags: [piNoEmpty]),
     (Name: ParamCommonComponents; Flags: []),
     (Name: ParamCommonTasks; Flags: []),
     (Name: ParamCommonLanguages; Flags: []),
@@ -4516,7 +4670,7 @@ const
     (Name: ParamCommonAfterInstall; Flags: []),
     (Name: ParamCommonMinVersion; Flags: []),
     (Name: ParamCommonOnlyBelowVersion; Flags: []));
-  Flags: array[0..40] of PChar = (
+  Flags: array[0..41] of PChar = (
     'confirmoverwrite', 'uninsneveruninstall', 'isreadme', 'regserver',
     'sharedfile', 'restartreplace', 'deleteafterinstall',
     'comparetimestamp', 'fontisnttruetype', 'regtypelib', 'external',
@@ -4527,7 +4681,8 @@ const
     'noencryption', 'nocompression', 'dontverifychecksum',
     'uninsnosharedfileprompt', 'createallsubdirs', '32bit', '64bit',
     'solidbreak', 'setntfscompression', 'unsetntfscompression',
-    'sortfilesbyname', 'gacinstall', 'sign', 'signonce', 'signcheck');
+    'sortfilesbyname', 'gacinstall', 'sign', 'signonce', 'signcheck',
+    'issigverify');
   SignFlags: array[TFileLocationSign] of String = (
     '', 'sign', 'signonce', 'signcheck');
   AttribsFlags: array[0..3] of PChar = (
@@ -4859,9 +5014,13 @@ type
               to compressing the first one }
             SolidBreak := False;
           end;
-        end;
+          NewFileLocationEntryExtraInfo^.ISSigAllowedKeys := NewFileEntry^.ISSigAllowedKeys;
+        end else if NewFileLocationEntryExtraInfo^.ISSigAllowedKeys <> NewFileEntry^.ISSigAllowedKeys then
+          AbortCompile(SCompilerFilesISSigAllowedKeysConflict);
         if Touch then
           Include(NewFileLocationEntryExtraInfo^.Flags, floApplyTouchDateTime);
+        if foISSigVerify in NewFileEntry^.Options then
+          Include(NewFileLocationEntryExtraInfo^.Flags, floISSigVerify);
         { Note: "nocompression"/"noencryption" on one file makes all merged
           copies uncompressed/unencrypted too }
         if NoCompression then
@@ -5135,6 +5294,7 @@ begin
                    38: ApplyNewSign(Sign, fsYes, SCompilerParamErrorBadCombo2);
                    39: ApplyNewSign(Sign, fsOnce, SCompilerParamErrorBadCombo2);
                    40: ApplyNewSign(Sign, fsCheck, SCompilerParamErrorBadCombo2);
+                   41: Include(Options, foISSigVerify);
                  end;
 
                { Source }
@@ -5220,6 +5380,25 @@ begin
                  Include(Options, foExternalSizePreset);
                end;
 
+               { ISSigAllowedKeys }
+               var S := Values[paISSigAllowedKeys].Data;
+               while True do begin
+                 const KeyNameOrGroupName = ExtractStr(S, ' ');
+                 if KeyNameOrGroupName = '' then
+                   Break;
+                 var FoundKey := False;
+                 for var KeyIndex := 0 to ISSigKeyEntryExtraInfos.Count-1 do begin
+                   var ISSigKeyEntryExtraInfo := PISSigKeyEntryExtraInfo(ISSigKeyEntryExtraInfos[KeyIndex]);
+                   if SameText(ISSigKeyEntryExtraInfo.Name, KeyNameOrGroupName) or
+                      ISSigKeyEntryExtraInfo.HasGroupName(KeyNameOrGroupName) then begin
+                     SetISSigAllowedKey(ISSigAllowedKeys, KeyIndex);
+                     FoundKey := True;
+                   end;
+                 end;
+                 if not FoundKey then
+                   AbortCompileFmt(SCompilerFilesUnkownISSigKeyNameOrGroupName, [ParamFilesISSigAllowedKeys]);
+               end;
+
                { Common parameters }
                ProcessExpressionParameter(ParamCommonComponents, Values[paComponents].Data, EvalComponentIdentifier, True, Components);
                ProcessExpressionParameter(ParamCommonTasks, Values[paTasks].Data, EvalTaskIdentifier, True, Tasks);
@@ -5285,9 +5464,19 @@ begin
             AbortCompileFmt(SCompilerParamErrorBadCombo2,
               [ParamCommonFlags, 'external', SignFlags[Sign]]);
         end;
-        
-        if (SignTools.Count = 0) and (Sign in [fsYes, fsOnce]) then
-          Sign := fsNoSetting;
+
+        if (ISSigKeyEntries.Count = 0) and (foISSigVerify in Options) then
+          AbortCompile(SCompilerFilesISSigVerifyMissingISSigKeys);
+        if (ISSigAllowedKeys <> '') and not (foISSigVerify in Options) then
+          AbortCompile(SCompilerFilesISSigAllowedKeysMissingISSigVerify);
+
+        if Sign in [fsYes, fsOnce] then begin
+          if foISSigVerify in Options then
+            AbortCompileFmt(SCompilerParamErrorBadCombo2,
+              [ParamCommonFlags, SignFlags[Sign], 'issigverify'])
+          else if SignTools.Count = 0 then
+            Sign := fsNoSetting
+        end;
 
         if not RecurseSubdirs and (foCreateAllSubDirs in Options) then
           AbortCompileFmt(SCompilerParamFlagMissing, ['recursesubdirs', 'createallsubdirs']);
@@ -5625,7 +5814,7 @@ begin
 
     { Name }
     if not IsValidIdentString(Values[paName].Data, False, False) then
-      AbortCompile(SCompilerLanguagesBadName);
+      AbortCompile(SCompilerLanguagesOrISSigKeysBadName);
     NewPreLangData.Name := Values[paName].Data;
 
     { MessagesFile }
@@ -5659,7 +5848,7 @@ begin
 
     { Name }
     if not IsValidIdentString(Values[paName].Data, False, False) then
-      AbortCompile(SCompilerLanguagesBadName);
+      AbortCompile(SCompilerLanguagesOrISSigKeysBadName);
     NewLanguageEntry.Name := Values[paName].Data;
 
     { MessagesFile }
@@ -6611,6 +6800,7 @@ var
     SetupHeader.NumComponentEntries := ComponentEntries.Count;
     SetupHeader.NumTaskEntries := TaskEntries.Count;
     SetupHeader.NumDirEntries := DirEntries.Count;
+    SetupHeader.NumISSigKeyEntries := ISSigKeyEntries.Count;
     SetupHeader.NumFileEntries := FileEntries.Count;
     SetupHeader.NumFileLocationEntries := FileLocationEntries.Count;
     SetupHeader.NumIconEntries := IconEntries.Count;
@@ -6652,6 +6842,9 @@ var
       for J := 0 to DirEntries.Count-1 do
         SECompressedBlockWrite(W, DirEntries[J]^, SizeOf(TSetupDirEntry),
           SetupDirEntryStrings, SetupDirEntryAnsiStrings);
+      for J := 0 to ISSigKeyEntries.Count-1 do
+        SECompressedBlockWrite(W, ISSigKeyEntries[J]^, SizeOf(TSetupISSigKeyEntry),
+          SetupISSigKeyEntryStrings, SetupISSigKeyEntryAnsiStrings);
       for J := 0 to FileEntries.Count-1 do
         SECompressedBlockWrite(W, FileEntries[J]^, SizeOf(TSetupFileEntry),
           SetupFileEntryStrings, SetupFileEntryAnsiStrings);
@@ -6839,6 +7032,7 @@ var
     SourceFile: TFile;
     SignatureAddress, SignatureSize: Cardinal;
     HdrChecksum, ErrorCode: DWORD;
+    ISSigAvailableKeys: TArrayOfECDSAKey;
   begin
     if (SetupHeader.CompressMethod in [cmLZMA, cmLZMA2]) and
        (CompressProps.WorkerProcessFilename <> '') then
@@ -6852,7 +7046,20 @@ var
 
     ChunkCompressed := False;  { avoid warning }
     CH := TCompressionHandler.Create(Self, FirstDestFile);
+    SetLength(ISSigAvailableKeys, ISSigKeyEntries.Count);
+    for I := 0 to ISSigKeyEntries.Count-1 do
+      ISSigAvailableKeys[I] := nil;
     try
+      for I := 0 to ISSigKeyEntries.Count-1 do begin
+        const ISSigKeyEntry = PSetupISSigKeyEntry(ISSigKeyEntries[I]);
+        ISSigAvailableKeys[I] := TECDSAKey.Create;
+        try
+          ISSigImportPublicKey(ISSigAvailableKeys[I], '', ISSigKeyEntry.PublicX, ISSigKeyEntry.PublicY); { shouldn't fail: values checked already }
+        except
+          AbortCompileFmt(SCompilerCompressInternalError, ['ISSigImportPublicKey failed: ' + GetExceptMessage]);
+        end;
+      end;
+
       if DiskSpanning then begin
         if not CH.ReserveBytesOnSlice(BytesToReserveOnFirstDisk) then
           AbortCompile(SCompilerNotEnoughSpaceOnFirstDisk);
@@ -6905,6 +7112,36 @@ var
         SourceFile := TFile.Create(FileLocationEntryFilenames[I],
           fdOpenExisting, faRead, fsRead);
         try
+          var ExpectedFileHash: TSHA256Digest;
+          if floISSigVerify in FLExtraInfo.Flags then begin
+            { See Setup.Install's CopySourceFileToDestFile for similar code }
+            if Length(ISSigAvailableKeys) = 0 then { shouldn't fail: flag stripped already }
+              AbortCompileFmt(SCompilerCompressInternalError, ['Length(ISSigAvailableKeys) = 0']);
+            const SigFilename = FileLocationEntryFilenames[I] + '.issig';
+            if not NewFileExists(SigFilename) then
+              AbortCompileFmt(SCompilerSourceFileISSigMissingFile, [FileLocationEntryFilenames[I]]);
+            const SigText = ISSigLoadTextFromFile(SigFilename);
+            var ExpectedFileSize: Int64;
+            const VerifyResult = ISSigVerifySignatureText(
+              GetISSigAllowedKeys(ISSigAvailableKeys, FLExtraInfo.ISSigAllowedKeys), SigText,
+              ExpectedFileSize, ExpectedFileHash, FLExtraInfo.ISSigKeyUsedID);
+            if VerifyResult <> vsrSuccess then begin
+              var VerifyResultAsString: String;
+              case VerifyResult of
+                vsrMalformed, vsrBadSignature: VerifyResultAsString := SCompilerSourceFileISSigMalformedOrBadSignature;
+                vsrKeyNotFound: VerifyResultAsString := SCompilerSourceFileISSigKeyNotFound;
+              else
+                VerifyResultAsString := SCompilerSourceFileISSigUnknownVerifyResult;
+              end;
+              AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature,
+                [FileLocationEntryFilenames[I], VerifyResultAsString]);
+            end;
+            if Int64(SourceFile.Size) <> ExpectedFileSize then
+              AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature,
+                [FileLocationEntryFilenames[I], SCompilerSourceFileISSigFileSizeIncorrect]);
+            { ExpectedFileHash checked below after compression }
+          end;
+
           if CH.ChunkStarted then begin
             { End the current chunk if one of the following conditions is true:
               - we're not using solid compression
@@ -6950,6 +7187,13 @@ var
 
           CH.CompressFile(SourceFile, FL.OriginalSize,
             floCallInstructionOptimized in FL.Flags, FL.SHA256Sum);
+
+          if floISSigVerify in FLExtraInfo.Flags then begin
+            if not SHA256DigestsEqual(FL.SHA256Sum, ExpectedFileHash) then
+              AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature,
+                [FileLocationEntryFilenames[I], SCompilerSourceFileISSigFileHashIncorrect]);
+            AddStatus(SCompilerStatusFilesISSigVerified);
+          end;
         finally
           SourceFile.Free;
         end;
@@ -6960,6 +7204,8 @@ var
       CH.Finish;
     finally
       CompressionInProgress := False;
+      for I := 0 to Length(ISSigAvailableKeys)-1 do
+        ISSigAvailableKeys[I].Free;
       CH.Free;
     end;
 
@@ -7215,6 +7461,7 @@ var
   var
     F: TTextFileWriter;
     FL: PSetupFileLocationEntry;
+    FLExtraInfo: PFileLocationEntryExtraInfo;
     S: String;
     I: Integer;
   begin
@@ -7224,11 +7471,13 @@ var
       S := 'Index' + #9 + 'SourceFilename' + #9 + 'TimeStamp' + #9 +
         'Version' + #9 + 'SHA256Sum' + #9 + 'OriginalSize' + #9 +
         'FirstSlice' + #9 + 'LastSlice' + #9 + 'StartOffset' + #9 +
-        'ChunkSuboffset' + #9 + 'ChunkCompressedSize' + #9 + 'Encrypted';
+        'ChunkSuboffset' + #9 + 'ChunkCompressedSize' + #9 + 'Encrypted' + #9 +
+        'ISSigKeyID';
       F.WriteLine(S);
 
       for I := 0 to FileLocationEntries.Count-1 do begin
         FL := FileLocationEntries[I];
+        FLExtraInfo := FileLocationEntryExtraInfos[I];
         S := IntToStr(I) + #9 + FileLocationEntryFilenames[I] + #9 +
           FileTimeToString(FL.SourceTimeStamp, floTimeStampInUTC in FL.Flags) + #9;
         if floVersionInfoValid in FL.Flags then
@@ -7242,7 +7491,8 @@ var
           IntToStr(FL.StartOffset) + #9 +
           Integer64ToStr(FL.ChunkSuboffset) + #9 +
           Integer64ToStr(FL.ChunkCompressedSize) + #9 +
-          EncryptedStrings[floChunkEncrypted in FL.Flags];
+          EncryptedStrings[floChunkEncrypted in FL.Flags] + #9 +
+          FLExtraInfo.ISSigKeyUsedID;
         F.WriteLine(S);
       end;
     finally
@@ -7808,6 +8058,10 @@ begin
     if MissingRunOnceIdsWarning and MissingRunOnceIds then
       WarningsList.Add(Format(SCompilerMissingRunOnceIdsWarning, ['UninstallRun', 'RunOnceId']));
 
+    { Read [ISSigKeys] section - must be done before reading [Files] section }
+    EnumIniSection(EnumISSigKeysProc, 'ISSigKeys', 0, True, True, '', False, False);
+    CallIdleProc;
+
     { Read [Files] section }
     if not TryStrToBoolean(SetupHeader.Uninstallable, Uninstallable) or Uninstallable then
       EnumFilesProc('', 1);
@@ -8029,6 +8283,7 @@ begin
     ClearSEList(DirEntries, SetupDirEntryStrings, SetupDirEntryAnsiStrings);
     ClearSEList(FileEntries, SetupFileEntryStrings, SetupFileEntryAnsiStrings);
     ClearSEList(FileLocationEntries, SetupFileLocationEntryStrings, SetupFileLocationEntryAnsiStrings);
+    ClearSEList(ISSigKeyEntries, SetupISSigKeyEntryStrings, SetupISSigKeyEntryAnsiStrings);
     ClearSEList(IconEntries, SetupIconEntryStrings, SetupIconEntryAnsiStrings);
     ClearSEList(IniEntries, SetupIniEntryStrings, SetupIniEntryAnsiStrings);
     ClearSEList(RegistryEntries, SetupRegistryEntryStrings, SetupRegistryEntryAnsiStrings);
@@ -8041,6 +8296,10 @@ begin
       Dispose(PFileLocationEntryExtraInfo(FileLocationEntryExtraInfos[I]));
       FileLocationEntryExtraInfos.Delete(I);
     end;
+    for I := ISSigKeyEntryExtraInfos.Count-1 downto 0 do begin
+      Dispose(PISSigKeyEntryExtraInfo(ISSigKeyEntryExtraInfos[I]));
+      ISSigKeyEntryExtraInfos.Delete(I);
+    end;
     ClearLineInfoList(ExpectedCustomMessageNames);
     ClearLangDataList;
     ClearPreLangDataList;

+ 6 - 0
Projects/Src/Compression.LZMA1SmallDecompressor/LzmaDecode/LzmaDecodeInno.obj.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 8111
+file-hash 25a946de5cd685e1b010293665ee04eaa24ac9a345a31afaa82217e273459b2c
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r ebd4a121bba6483e335759c41c285aa582a3d9d1fe5aa0261d86a187d19fc035
+sig-s c0f9e67aaa8c548bf13edb5f4edb1ada130aaa0355e3e702bc837f3121be964c

+ 6 - 0
Projects/Src/Compression.LZMADecompressor/Lzma2Decode/ISLzmaDec.obj.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 21751
+file-hash 79bb11fbe0b862b6a42af2db4664748daa54347d56b3c40998dcb860111195ac
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 9dd4d53f2270c6a0f415fcc553e561b67cc0ba9f067c497f9aa4c40625263737
+sig-s ad98a5f58bff21054c51bc25f4c95fad3e7f669bf1b7068901a52b503414516b

+ 6 - 0
Projects/Src/Compression.SevenZipDecoder/7zDecode/IS7zDec.obj.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 93601
+file-hash aad21cad7686f881b461c5727995391a00b48df251893a97e2cb0f9567c10a00
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r 72598c2e35dc647f0da0c714648ffb30271e6c4e96816aa651fd7d52b584b1e5
+sig-s db13c937f9bce1bf736b57cf62c30d836ae8604bf11a66f69443a6163864dfea

+ 12 - 4
Projects/Src/IDE.ScintStylerInnoSetup.pas

@@ -46,6 +46,7 @@ type
     scComponents,
     scCustomMessages,
     scDirs,
+    scISSigKeys,
     scFiles,
     scIcons,
     scINI,
@@ -183,11 +184,12 @@ type
   end;
 
 const
-  SectionMap: array[0..17] of TSectionMapItem = (
+  SectionMap: array[0..18] of TSectionMapItem = (
     (Name: 'Code'; Section: scCode),
     (Name: 'Components'; Section: scComponents),
     (Name: 'CustomMessages'; Section: scCustomMessages),
     (Name: 'Dirs'; Section: scDirs),
+    (Name: 'ISSigKeys'; Section: scISSigKeys),
     (Name: 'Files'; Section: scFiles),
     (Name: 'Icons'; Section: scIcons),
     (Name: 'INI'; Section: scINI),
@@ -232,17 +234,21 @@ const
     'uninsneveruninstall', 'unsetntfscompression'
   ];
 
+  ISSigKeysSectionParameters: array of TScintRawString = [
+    'Name', 'Group', 'KeyFile', 'KeyID', 'PublicX', 'PublicY'
+  ];
+
   FilesSectionParameters: array of TScintRawString = [
     'AfterInstall', 'Attribs', 'BeforeInstall', 'Check', 'Components', 'CopyMode',
     'DestDir', 'DestName', 'Excludes', 'ExternalSize', 'Flags', 'FontInstall',
-    'Languages', 'MinVersion', 'OnlyBelowVersion', 'Permissions', 'Source',
-    'StrongAssemblyName', 'Tasks'
+    'ISSigAllowedKeys', 'Languages', 'MinVersion', 'OnlyBelowVersion', 'Permissions',
+    'Source', 'StrongAssemblyName', 'Tasks'
   ];
 
   FilesSectionFlags: array of TScintRawString = [
     '32bit', '64bit', 'allowunsafefiles', 'comparetimestamp', 'confirmoverwrite',
     'createallsubdirs', 'deleteafterinstall', 'dontcopy', 'dontverifychecksum',
-    'external', 'fontisnttruetype', 'gacinstall', 'ignoreversion', 'isreadme',
+    'external', 'fontisnttruetype', 'gacinstall', 'ignoreversion', 'isreadme', 'issigverify',
     'nocompression', 'noencryption', 'noregerror', 'onlyifdestfileexists',
     'onlyifdoesntexist', 'overwritereadonly', 'promptifolder', 'recursesubdirs',
     'regserver', 'regtypelib', 'replacesameversion', 'restartreplace',
@@ -603,6 +609,7 @@ constructor TInnoSetupStyler.Create(AOwner: TComponent);
 
   procedure BuildKeywordsWordLists;
   begin
+    BuildKeywordsWordList(scISSigKeys, ISSigKeysSectionParameters);
     BuildKeywordsWordList(scFiles, FilesSectionParameters);
     BuildKeywordsWordList(scComponents, ComponentsSectionParameters);
     BuildKeywordsWordList(scDirs, DirsSectionParameters);
@@ -1719,6 +1726,7 @@ begin
       scComponents: HandleParameterSection(ComponentsSectionParameters);
       scCustomMessages: HandleKeyValueSection(Section);
       scDirs: HandleParameterSection(DirsSectionParameters);
+      scISSigKeys: HandleParameterSection(ISSigKeysSectionParameters);
       scFiles: HandleParameterSection(FilesSectionParameters);
       scIcons: HandleParameterSection(IconsSectionParameters);
       scINI: HandleParameterSection(INISectionParameters);

+ 6 - 0
Projects/Src/Setup.HelperEXEs.res.issig

@@ -0,0 +1,6 @@
+format issig-v1
+file-size 6240
+file-hash 0c384c152e4686c6a6a1dbdb1376f9076baa13ad7d6db9891745187f8212f393
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+sig-r ae63371b72241fb9bc4d2ada583a4ff3443261a59885fe5e0b5362c41dde704e
+sig-s 529f2dd4c752daa00e78cdb19579bc22a404b578dcd487209b93580ee652bce9

+ 71 - 5
Projects/Src/Setup.Install.pas

@@ -2,7 +2,7 @@ unit Setup.Install;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -31,7 +31,8 @@ uses
   Windows, SysUtils, Messages, Classes, Forms, ShlObj, Shared.Struct, Setup.UninstallLog, Shared.SetupTypes,
   SetupLdrAndSetup.InstFunc, Setup.InstFunc, Setup.InstFunc.Ole, Setup.SecurityFunc, SetupLdrAndSetup.Messages,
   Setup.MainFunc, Setup.LoggingFunc, Setup.FileExtractor, Shared.FileClass,
-  Compression.Base, SHA256, PathFunc, Shared.CommonFunc.Vcl, Shared.CommonFunc, SetupLdrAndSetup.RedirFunc, Shared.Int64Em, Shared.SetupMessageIDs,
+  Compression.Base, SHA256, PathFunc, ECDSA, ISSigFunc, Shared.CommonFunc.Vcl,
+  Shared.CommonFunc, SetupLdrAndSetup.RedirFunc, Shared.Int64Em, Shared.SetupMessageIDs,
   Setup.WizardForm, Shared.DebugStruct, Setup.DebugClient, Shared.VerInfoFunc, Setup.ScriptRunner, Setup.RegDLL, Setup.Helper,
   Shared.ResUpdateFunc, Setup.DotNetFunc, TaskbarProgressFunc, NewProgressBar, RestartManager,
   Net.HTTPClient, Net.URLClient, NetEncoding, RegStr;
@@ -249,16 +250,58 @@ begin
   end;
 end;
 
+procedure ISSigVerifyError(const AReason, AExceptionMessage: String);
+begin
+  Log('ISSig verification error: ' + AddPeriod(AReason));
+  raise Exception.Create(AExceptionMessage);
+end;
+
 procedure CopySourceFileToDestFile(const SourceF, DestF: TFile;
-  AMaxProgress: Integer64);
+  const ISSigVerify: Boolean; [Ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
+  const ISSigAllowedKeys: AnsiString; const ISSigFilename: String; AMaxProgress: Integer64);
 { Copies all bytes from SourceF to DestF, incrementing process meter as it
   goes. Assumes file pointers of both are 0. }
+const
+  ISSigMissingFile = 'Signature file does not exist';
+  ISSigMalformedOrBadSignature = 'Malformed or bad signature';
+  ISSigKeyNotFound = 'No matching key found';
+  ISSigUnknownVerifyResult  = 'Unknown verify result';
+  ISSigFileSizeIncorrect = 'File size incorrect';
+  ISSigFileHashIncorrect = 'File hash incorrect';
 var
   BytesLeft: Integer64;
   NewProgress: Integer64;
   BufSize: Cardinal;
   Buf: array[0..16383] of Byte;
+  Context: TSHA256Context;
 begin
+  var ExpectedFileHash: TSHA256Digest;
+  if ISSigVerify then begin
+    { See Compiler.SetupCompiler's TSetupCompiler.Compile for similar code }
+    if not NewFileExists(ISSigFilename) then
+      ISSigVerifyError(ISSigMissingFile, FmtSetupMessage1(msgSourceDoesntExist, ISSigFilename));
+    const SigText = ISSigLoadTextFromFile(ISSigFilename);
+    var ExpectedFileSize: Int64;
+    const VerifyResult = ISSigVerifySignatureText(
+      GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys), SigText,
+      ExpectedFileSize, ExpectedFileHash);
+    if VerifyResult <> vsrSuccess then begin
+      var VerifyResultAsString: String;
+      case VerifyResult of
+        vsrMalformed, vsrBadSignature: VerifyResultAsString := ISSigMalformedOrBadSignature;
+        vsrKeyNotFound: VerifyResultAsString := ISSigKeyNotFound;
+      else
+        VerifyResultAsString := ISSigUnknownVerifyResult;
+      end;
+      ISSigVerifyError(VerifyResultAsString, SetupMessages[msgSourceIsCorrupted]);
+    end;
+    if Int64(SourceF.Size) <> ExpectedFileSize then
+      ISSigVerifyError(ISSigFileSizeIncorrect, SetupMessages[msgSourceIsCorrupted]);
+    { ExpectedFileHash checked below after copy }
+
+    SHA256Init(Context);
+  end;
+
   Inc6464(AMaxProgress, CurProgress);
   BytesLeft := SourceF.Size;
 
@@ -279,6 +322,9 @@ begin
     DestF.WriteBuffer(Buf, BufSize);
     Dec64(BytesLeft, BufSize);
 
+    if ISSigVerify then
+      SHA256Update(Context, Buf, BufSize);
+
     NewProgress := CurProgress;
     Inc64(NewProgress, BufSize);
     if Compare64(NewProgress, AMaxProgress) > 0 then
@@ -288,6 +334,12 @@ begin
     ProcessEvents;
   end;
 
+  if ISSigVerify then begin
+    if not SHA256DigestsEqual(SHA256Final(Context), ExpectedFileHash) then
+      ISSigVerifyError(ISSigFileHashIncorrect, SetupMessages[msgSourceIsCorrupted]);
+    Log('ISSig verification successful.');
+  end;
+
   { In case the source file was shorter than we thought it was, bump the
     progress bar to the maximum amount }
   SetProgress(AMaxProgress);
@@ -341,6 +393,7 @@ var
   UninstallDataCreated, UninstallMsgCreated, AppendUninstallData: Boolean;
   RegisterFilesList: TList;
   ExpandedAppId: String;
+  ISSigAvailableKeys: TArrayOfECDSAKey;
 
   function GetLocalTimeAsStr: String;
   var
@@ -1441,9 +1494,11 @@ var
               try
                 LastOperation := SetupMessages[msgErrorCopying];
                 if Assigned(CurFileLocation) then
-                  CopySourceFileToDestFile(SourceF, DestF, CurFileLocation^.OriginalSize)
+                  CopySourceFileToDestFile(SourceF, DestF, False,
+                    [], '', '', CurFileLocation^.OriginalSize)
                 else
-                  CopySourceFileToDestFile(SourceF, DestF, AExternalSize);
+                  CopySourceFileToDestFile(SourceF, DestF, foISSigVerify in CurFile^.Options,
+                    ISSigAvailableKeys, CurFile^.ISSigAllowedKeys, SourceFile + '.issig', AExternalSize);
               finally
                 SourceF.Free;
               end;
@@ -3063,6 +3118,7 @@ begin
   AppendUninstallData := False;
   UninstLogCleared := False;
   RegisterFilesList := nil;
+  SetLength(ISSigAvailableKeys, 0);
   UninstLog := TSetupUninstallLog.Create;
   try
     try
@@ -3100,6 +3156,14 @@ begin
 
       RegisterFilesList := TList.Create;
 
+      SetLength(ISSigAvailableKeys, Entries[seISSigKey].Count);
+      for var N := 0 to Entries[seISSigKey].Count-1 do begin
+        var ISSigKeyEntry := PSetupISSigKeyEntry(Entries[seISSigKey][N]);
+        ISSigAvailableKeys[N] := TECDSAKey.Create;
+        if ISSigImportPublicKey(ISSigAvailableKeys[N], '', ISSigKeyEntry.PublicX, ISSigKeyEntry.PublicY) <> ikrSuccess then
+          InternalError('ISSigImportPublicKey failed')
+      end;
+
       { Process Component entries, if any }
       ProcessComponentEntries;
       ProcessEvents;
@@ -3260,6 +3324,8 @@ begin
       Exit;
     end;
   finally
+    for I := 0 to Length(ISSigAvailableKeys)-1 do
+      ISSigAvailableKeys[I].Free;
     if Assigned(RegisterFilesList) then begin
       for I := RegisterFilesList.Count-1 downto 0 do
         Dispose(PRegisterFilesListRec(RegisterFilesList[I]));

+ 11 - 9
Projects/Src/Setup.MainFunc.pas

@@ -18,7 +18,7 @@ uses
 
 type
   TEntryType = (seLanguage, seCustomMessage, sePermission, seType, seComponent,
-    seTask, seDir, seFile, seFileLocation, seIcon, seIni, seRegistry,
+    seTask, seDir, seISSigKey, seFile, seFileLocation, seIcon, seIni, seRegistry,
     seInstallDelete, seUninstallDelete, seRun, seUninstallRun);
 
   TShellFolderID = (sfDesktop, sfStartMenu, sfPrograms, sfStartup, sfSendTo,  //these have common and user versions
@@ -29,18 +29,18 @@ const
   EntryStrings: array[TEntryType] of Integer = (SetupLanguageEntryStrings,
     SetupCustomMessageEntryStrings, SetupPermissionEntryStrings,
     SetupTypeEntryStrings, SetupComponentEntryStrings, SetupTaskEntryStrings,
-    SetupDirEntryStrings, SetupFileEntryStrings, SetupFileLocationEntryStrings,
-    SetupIconEntryStrings, SetupIniEntryStrings, SetupRegistryEntryStrings,
-    SetupDeleteEntryStrings, SetupDeleteEntryStrings, SetupRunEntryStrings,
-    SetupRunEntryStrings);
+    SetupDirEntryStrings, SetupISSigKeyEntryStrings, SetupFileEntryStrings,
+    SetupFileLocationEntryStrings, SetupIconEntryStrings, SetupIniEntryStrings,
+    SetupRegistryEntryStrings, SetupDeleteEntryStrings, SetupDeleteEntryStrings,
+    SetupRunEntryStrings, SetupRunEntryStrings);
 
   EntryAnsiStrings: array[TEntryType] of Integer = (SetupLanguageEntryAnsiStrings,
     SetupCustomMessageEntryAnsiStrings, SetupPermissionEntryAnsiStrings,
     SetupTypeEntryAnsiStrings, SetupComponentEntryAnsiStrings, SetupTaskEntryAnsiStrings,
-    SetupDirEntryAnsiStrings, SetupFileEntryAnsiStrings, SetupFileLocationEntryAnsiStrings,
-    SetupIconEntryAnsiStrings, SetupIniEntryAnsiStrings, SetupRegistryEntryAnsiStrings,
-    SetupDeleteEntryAnsiStrings, SetupDeleteEntryAnsiStrings, SetupRunEntryAnsiStrings,
-    SetupRunEntryAnsiStrings);
+    SetupDirEntryAnsiStrings, SetupISSigKeyEntryAnsiStrings, SetupFileEntryAnsiStrings,
+    SetupFileLocationEntryAnsiStrings, SetupIconEntryAnsiStrings, SetupIniEntryAnsiStrings,
+    SetupRegistryEntryAnsiStrings, SetupDeleteEntryAnsiStrings, SetupDeleteEntryAnsiStrings,
+    SetupRunEntryAnsiStrings, SetupRunEntryAnsiStrings);
 
   { Exit codes that are assigned to the SetupExitCode variable.
     Note: SetupLdr also returns exit codes with the same numbers. }
@@ -3067,6 +3067,8 @@ begin
         ReadEntries(seDir, SetupHeader.NumDirEntries, SizeOf(TSetupDirEntry),
           Integer(@PSetupDirEntry(nil).MinVersion),
           Integer(@PSetupDirEntry(nil).OnlyBelowVersion));
+        { ISSigKey entries }
+        ReadEntriesWithoutVersion(seISSigKey, SetupHeader.NumISSigKeyEntries, SizeOf(TSetupISSigKeyEntry));
         { File entries }
         ReadEntries(seFile, SetupHeader.NumFileEntries, SizeOf(TSetupFileEntry),
           Integer(@PSetupFileEntry(nil).MinVersion),

+ 46 - 2
Projects/Src/Shared.SetupTypes.pas

@@ -2,7 +2,7 @@ unit Shared.SetupTypes;
 
 {
   Inno Setup
-  Copyright (C) 1997-2018 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -12,7 +12,7 @@ unit Shared.SetupTypes;
 interface
 
 uses
-  SysUtils, Classes, Shared.Struct;
+  SysUtils, Classes, ECDSA, Shared.Struct;
 
 const
   { Predefined page identifiers }
@@ -37,6 +37,8 @@ type
 
   TRenamedConstantCallBack = procedure(const Cnst, CnstRenamed: String) of object;
 
+  TArrayOfECDSAKey = array of TECDSAKey;
+
 const
   crHand = 1;
 
@@ -53,6 +55,9 @@ function StrToSetupVersionData(const S: String; var VerData: TSetupVersionData):
 procedure HandleRenamedConstants(var Cnst: String; const RenamedConstantCallback: TRenamedConstantCallback);
 procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSalt;
   const Iterations: Integer; out Key: TSetupEncryptionKey);
+procedure SetISSigAllowedKey(var ISSigAllowedKeys: AnsiString; const KeyIndex: Integer);
+function GetISSigAllowedKeys([Ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
+  const ISSigAllowedKeys: AnsiString): TArrayOfECDSAKey;
 
 implementation
 
@@ -306,4 +311,43 @@ begin
   Move(KeyBytes[0], Key[0], KeyLength);
 end;
 
+{ Actually only used by ISCmplr and not by Setup - kept here anyway because IsISSigAllowedKey is}
+procedure SetISSigAllowedKey(var ISSigAllowedKeys: AnsiString; const KeyIndex: Integer);
+begin
+  { ISSigAllowedKeys should start out empty. If you then only use this function
+    to update it, regular string comparison can be used for comparisons. }
+  const ByteIndex = KeyIndex div 8;
+  while ByteIndex >= Length(ISSigAllowedKeys) do
+    ISSigAllowedKeys := ISSigAllowedKeys + #0;
+  const BitIndex = KeyIndex mod 8;
+  ISSigAllowedKeys[ByteIndex+1] := AnsiChar(Byte(ISSigAllowedKeys[ByteIndex+1]) or (1 shl BitIndex));
+end;
+
+function IsISSigAllowedKey(const ISSigAllowedKeys: AnsiString; const KeyIndex: Integer): Boolean;
+begin
+  const ByteIndex = KeyIndex div 8;
+  if ByteIndex >= Length(ISSigAllowedKeys) then
+    Exit(False);
+  const BitIndex = KeyIndex mod 8;
+  Result := Byte(ISSigAllowedKeys[ByteIndex+1]) and (1 shl BitIndex) <> 0;
+end;
+
+function GetISSigAllowedKeys([Ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
+  const ISSigAllowedKeys: AnsiString): TArrayOfECDSAKey;
+begin
+  if ISSigAllowedKeys <> '' then begin
+    const NAvailable = Length(ISSigAvailableKeys);
+    SetLength(Result, NAvailable);
+    var NAdded := 0;
+    for var I := 0 to NAvailable-1 do begin
+      if IsISSigAllowedKey(ISSigAllowedKeys, I) then begin
+        Result[NAdded] := ISSigAvailableKeys[I];
+        Inc(NAdded);
+      end;
+    end;
+    SetLength(Result, NAdded);
+  end else
+    Result := ISSigAvailableKeys;
+end;
+
 end.

+ 14 - 5
Projects/Src/Shared.Struct.pas

@@ -17,7 +17,7 @@ uses
 
 const
   SetupTitle = 'Inno Setup';
-  SetupVersion = '6.4.3';
+  SetupVersion = '6.5.0-dev';
   SetupBinVersion = (6 shl 24) + (4 shl 16) + (3 shl 8) + 0;
 
 type
@@ -33,7 +33,7 @@ const
     this file it's recommended you change SetupID. Any change will do (like
     changing the letters or numbers), as long as your format is
     unrecognizable by the standard Inno Setup. }
-  SetupID: TSetupID = 'Inno Setup Setup Data (6.4.3)';
+  SetupID: TSetupID = 'Inno Setup Setup Data (6.5.0)';
   UninstallLogID: array[Boolean] of TUninstallLogID =
     ('Inno Setup Uninstall Log (b)', 'Inno Setup Uninstall Log (b) 64-bit');
   MessagesHdrID: TMessagesHdrID = 'Inno Setup Messages (6.4.0) (u)';
@@ -101,7 +101,7 @@ type
     LicenseText, InfoBeforeText, InfoAfterText, CompiledCodeText: AnsiString;
     NumLanguageEntries, NumCustomMessageEntries, NumPermissionEntries,
       NumTypeEntries, NumComponentEntries, NumTaskEntries, NumDirEntries,
-      NumFileEntries, NumFileLocationEntries, NumIconEntries, NumIniEntries,
+      NumISSigKeyEntries, NumFileEntries, NumFileLocationEntries, NumIconEntries, NumIniEntries,
       NumRegistryEntries, NumInstallDeleteEntries, NumUninstallDeleteEntries,
       NumRunEntries, NumUninstallRunEntries: Integer;
     MinVersion, OnlyBelowVersion: TSetupVersionData;
@@ -217,14 +217,23 @@ type
     Options: set of (doUninsNeverUninstall, doDeleteAfterInstall,
       doUninsAlwaysUninstall, doSetNTFSCompression, doUnsetNTFSCompression);
   end;
+const
+  SetupISSigKeyEntryStrings = 2;
+  SetupISSigKeyEntryAnsiStrings = 0;
+type
+  PSetupISSigKeyEntry = ^TSetupISSigKeyEntry;
+  TSetupISSigKeyEntry = packed record
+    PublicX, PublicY: String;
+  end;
 const
   SetupFileEntryStrings = 10;
-  SetupFileEntryAnsiStrings = 0;
+  SetupFileEntryAnsiStrings = 1;
 type
   PSetupFileEntry = ^TSetupFileEntry;
   TSetupFileEntry = packed record
     SourceFilename, DestName, InstallFontName, StrongAssemblyName: String;
     Components, Tasks, Languages, Check, AfterInstall, BeforeInstall: String;
+    ISSigAllowedKeys: AnsiString;
     MinVersion, OnlyBelowVersion: TSetupVersionData;
     LocationEntry: Integer;
     Attribs: Integer;
@@ -240,7 +249,7 @@ type
       foRecurseSubDirsExternal, foReplaceSameVersionIfContentsDiffer,
       foDontVerifyChecksum, foUninsNoSharedFilePrompt, foCreateAllSubDirs,
       fo32Bit, fo64Bit, foExternalSizePreset, foSetNTFSCompression,
-      foUnsetNTFSCompression, foGacInstall);
+      foUnsetNTFSCompression, foGacInstall, foISSigVerify);
     FileType: (ftUserFile, ftUninstExe);
   end;
 const

+ 2 - 2
README.md

@@ -137,9 +137,9 @@ performs all (un)installation-related tasks.
 Setup program into the user's TEMP directory and runs it from there. It also
 displays the "This will install..." and /HELP message boxes.
 
-**ISSigTool** - This is a command-line tool which can be used to sign and verify
+**ISSigTool** - This is a command-line utility which can be used to sign and verify
 any file. Compil32, ISCC, and ISCmplr use these signatures to verify the
-authenticity of a number of DLL files before loading them. Note: this tool does
+authenticity of a number of DLL files before loading them. Note: this utility does
 not replace Microsoft's signtool.exe in any way and is in fact not related to
 Authenticode Code Signing at all.
 

+ 21 - 4
build-ce.bat

@@ -19,7 +19,7 @@ rem  Once done the installer can be found in Output
 
 setlocal
 
-set VER=6.4.3
+set VER=6.5.0-dev
 
 echo Building Inno Setup %VER%...
 echo.
@@ -33,11 +33,22 @@ if not exist files\issigtool.exe (
   echo Missing ISSigTool
   echo Now open Projects\Projects.groupproj and build ISSigTool in Release mode
 
-  echo Waiting for file...
+  echo - Waiting for file...
   call :waitforfile files\issigtool.exe
   echo Compiling ISSigTool done
 )
 
+rem  Verify precompiled binaries which are used during compilation
+rem  Note: Other precompiled binaries are verified by Setup.iss
+call .\issig.bat verify --key-file=issig.ispublickey1 ^
+  Projects\Src\Setup.HelperEXEs.res ^
+  Projects\Src\Compression.LZMADecompressor\Lzma2Decode\ISLzmaDec.obj ^
+  Projects\Src\Compression.LZMA1SmallDecompressor\LzmaDecode\LzmaDecodeInno.obj ^
+  Projects\Src\Compression.SevenZipDecoder\7zDecode\IS7zDec.obj
+if errorlevel 1 goto failed
+echo ISSigTool verify done
+
+rem  Embed user's public key into sources
 call .\issig.bat embed
 if errorlevel 1 goto failed
 echo ISSigTool embed done
@@ -55,7 +66,7 @@ call :deletefile ishelp\ishelpgen\ishelpgen.exe
 echo Clearing compilation output done
 echo Now open Projects\Projects.groupproj and build all projects in Release mode
 
-echo Waiting for files...
+echo - Waiting for files...
 call :waitforfile files\compil32.exe
 call :waitforfile files\iscc.exe
 call :waitforfile files\iscmplr.dll
@@ -70,10 +81,13 @@ timeout /t 2 /nobreak >nul
 echo Compiling Inno Setup done
 
 if exist .\setup-presign.bat (
+  echo - Presigning
   call .\setup-presign.bat Files\ISCC.exe Files\ISCmplr.dll Files\ISPP.dll
+  echo Presign done
 )
 
-call .\issig.bat sign
+rem  Sign using user's private key
+call .\issig.bat sign Files\ISCmplr.dll Files\ISPP.dll
 if errorlevel 1 goto failed
 echo ISSigTool sign done
 pause
@@ -103,7 +117,10 @@ if errorlevel 1 goto failed
 cd ..
 if errorlevel 1 goto failed
 echo Creating Inno Setup installer done
+call .\issig.bat sign output\innosetup-%VER%.exe
+if errorlevel 1 goto failed
 powershell.exe -NoProfile -Command "Write-Host -NoNewline 'SHA256 hash: '; (Get-FileHash -Algorithm SHA256 -Path output\innosetup-%VER%.exe).Hash.ToLower()"
+if errorlevel 1 goto failed
 
 echo All done!
 pause

+ 19 - 2
build.bat

@@ -20,7 +20,7 @@ rem  Once done the installer can be found in Output
 
 setlocal
 
-set VER=6.4.3
+set VER=6.5.0-dev
 
 echo Building Inno Setup %VER%...
 echo.
@@ -55,6 +55,17 @@ if not exist files\issigtool.exe (
   echo Compiling ISSigTool done
 )
 
+rem  Verify precompiled binaries which are used during compilation
+rem  Note: Other precompiled binaries are verified by Setup.iss
+call .\issig.bat verify --key-file=issig.ispublickey1 ^
+  Projects\Src\Setup.HelperEXEs.res ^
+  Projects\Src\Compression.LZMADecompressor\Lzma2Decode\ISLzmaDec.obj ^
+  Projects\Src\Compression.LZMA1SmallDecompressor\LzmaDecode\LzmaDecodeInno.obj ^
+  Projects\Src\Compression.SevenZipDecoder\7zDecode\IS7zDec.obj
+if errorlevel 1 goto failed
+echo ISSigTool verify done
+
+rem  Embed user's public key into sources
 call .\issig.bat embed
 if errorlevel 1 goto failed
 echo ISSigTool embed done
@@ -64,10 +75,13 @@ if errorlevel 1 goto failed
 echo Compiling Inno Setup done
 
 if exist .\setup-presign.bat (
+  echo - Presigning
   call .\setup-presign.bat Files\ISCC.exe Files\ISCmplr.dll Files\ISPP.dll
+  echo Presign done
 ) 
 
-call .\issig.bat sign
+rem  Sign using user's private key
+call .\issig.bat sign Files\ISCmplr.dll Files\ISPP.dll
 if errorlevel 1 goto failed
 echo ISSigTool sign done
 pause
@@ -88,7 +102,10 @@ if errorlevel 1 goto failed
 cd ..
 if errorlevel 1 goto failed
 echo Creating Inno Setup installer done
+call .\issig.bat sign output\innosetup-%VER%.exe
+if errorlevel 1 goto failed
 powershell.exe -NoProfile -Command "Write-Host -NoNewline 'SHA256 hash: '; (Get-FileHash -Algorithm SHA256 -Path output\innosetup-%VER%.exe).Hash.ToLower()"
+if errorlevel 1 goto failed
 
 echo All done!
 pause

+ 17 - 18
issig.bat

@@ -6,10 +6,12 @@ rem  Portions by Martijn Laan
 rem  For conditions of distribution and use, see LICENSE.TXT.
 rem
 rem  Batch file to embed the user's public key from compilesettings.bat in
-rem  TrustFunc.AllowedPublicKeys.inc (before compilation) or to sign files
-rem  using it (after compilation)
+rem  TrustFunc.AllowedPublicKeys.inc and setup.allowedpublickeys.iss (before
+rem  compilation) or to sign files using it (after compilation)
 rem
 rem  If the key is missing it will be generated
+rem
+rem  Also used by build(-ce).bat to verify some precompiled files
 
 setlocal
 
@@ -30,42 +32,40 @@ if "%ISSIGTOOL_KEY_FILE%"=="" goto compilesettingserror
 
 rem -------------------------------------------------------------------------
 
-cd Files
-if errorlevel 1 goto failed
-
 if not exist "%ISSIGTOOL_KEY_FILE%" (
   echo Missing key file
-  ISSigTool.exe generate-private-key
+  Files\ISSigTool.exe generate-private-key
   if errorlevel 1 goto failed
   if not exist "%ISSIGTOOL_KEY_FILE%" goto failed
   echo Generating key file done - do not share with others!
 )
 
 if "%1"=="embed" goto embed
-if "%1"=="sign" goto sign
+if "%1"=="sign" goto signorverify
+if "%1"=="verify" goto signorverify
 if not "%1"=="" goto failed
 
 :embed
-set targetfile=..\Components\TrustFunc.AllowedPublicKeys.inc
-if not exist "%targetfile%" goto failed
 set publickeyfile=_temp.ispublickey
-ISSigTool.exe export-public-key "%publickeyfile%"
+Files\ISSigTool.exe export-public-key "%publickeyfile%"
 if errorlevel 1 goto failed
 if not exist "%publickeyfile%" goto failed
-powershell.exe -NoProfile -Command "$filePath = '%targetfile%'; $replacementFilePath = '%publickeyfile%'; $startMarker = 'AllowedPublicKey2Text :='; $endMarker = ';'; try { $content = Get-Content -Raw -Path $filePath; $replacementText = Get-Content -Raw -Path $replacementFilePath; $replacementText = $replacementText -replace \"`r`n\", \"' + #13#10 +`r`n'\"; $replacementText = \"'\" + $replacementText + \"'\"; $replacementText = $replacementText -replace \" \+`r`n''\", \"\"; [string] $pattern = '(?s)' + [regex]::Escape($startMarker) + '.*?' + [regex]::Escape($endMarker); if ($content -match $pattern) { $replacement = $startMarker + \"`r`n\" + $replacementText  + $endMarker; $newContent = $content -replace $pattern, $replacement; $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($false); [System.IO.File]::WriteAllText($filePath, $newContent, $utf8NoBomEncoding); Write-Host 'Embedded key.'; } else { Write-Host 'Markers not found.'; exit 1; } } catch { Write-Error ('Error: ' + $_.Exception.Message); exit 1; }"
+set targetfile=Components\TrustFunc.AllowedPublicKeys.inc
+if not exist "%targetfile%" goto failed
+powershell.exe -NoProfile -Command "$filePath = '%targetfile%'; $replacementFilePath = '%publickeyfile%'; $startMarker = 'AllowedPublicKey2Text :='; $endMarker = ';'; try { $content = Get-Content -Raw -Path $filePath; $replacementText = Get-Content -Raw -Path $replacementFilePath; $replacementText = $replacementText -replace \"`r`n\", \"' + #13#10 +`r`n'\"; $replacementText = \"'\" + $replacementText + \"'\"; $replacementText = $replacementText -replace \" \+`r`n''\", \"\"; [string] $pattern = '(?s)' + [regex]::Escape($startMarker) + '.*?' + [regex]::Escape($endMarker); if ($content -match $pattern) { $replacement = $startMarker + \"`r`n\" + $replacementText  + $endMarker; $newContent = $content -replace $pattern, $replacement; $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($false); [System.IO.File]::WriteAllText($filePath, $newContent, $utf8NoBomEncoding); Write-Host \"Embedded public key in $filePath.\"; } else { Write-Host \"Pattern not found in $filePath.\"; exit 1; } } catch { Write-Error (\"Error: $_.Exception.Message\"); exit 1; }"
 if errorlevel 1 goto failed
-del "%publickeyfile%"
+set targetfile=setup.allowedpublickeys.iss
+if not exist "%targetfile%" goto failed
+powershell.exe -NoProfile -Command "$filePath = '%targetfile%'; $replacementFilePath = '%publickeyfile%'; $startMarker = 'Name: mykey2; '; try { $content = Get-Content -Raw -Path $filePath; $replacementText = Get-Content -Raw -Path $replacementFilePath; $replacementText = $replacementText -replace \"`r`n\", \"; \"; $replacementText = $replacementText.Substring(0, $replacementText.Length - 2); $replacementText = $replacementText -replace 'format issig-public-key; key-id', 'KeyID:'; $replacementText = $replacementText -replace 'public-x', 'PublicX:'; $replacementText = $replacementText -replace 'public-y', 'PublicY:'; [string] $pattern = [regex]::Escape($startMarker) + '.*?$'; if ($content -match $pattern) { $replacement = $startMarker + $replacementText; $newContent = $content -replace $pattern, $replacement; $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($false); [System.IO.File]::WriteAllText($filePath, $newContent, $utf8NoBomEncoding); Write-Host \"Embedded public key in $filePath.\"; } else { Write-Host \"Pattern not found in $filePath.\"; exit 1; } } catch { Write-Error (\"Error: $_.Exception.Message\"); exit 1; }"
 if errorlevel 1 goto failed
-cd ..
+del "%publickeyfile%"
 if errorlevel 1 goto failed
 
 echo Success!
 goto exit
 
-:sign
-ISSigTool.exe sign ISCmplr.dll ISPP.dll
-if errorlevel 1 goto failed
-cd ..
+:signorverify
+Files\ISSigTool.exe %*
 if errorlevel 1 goto failed
 
 echo Success!
@@ -73,7 +73,6 @@ goto exit
 
 :failed
 echo *** FAILED ***
-cd ..
 :failed2
 exit /b 1
 

+ 4 - 0
issig.ispublickey1

@@ -0,0 +1,4 @@
+format issig-public-key
+key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
+public-x e3e943066aff8f28d2219fd71c9ffff4c8d1aa26bc4225434be67180ab5e242d
+public-y e419041c3f54551e86a1c47f387005cd535dfc9d64339b30d37f9a4f7866b650

+ 7 - 0
setup.allowedpublickeys.iss

@@ -0,0 +1,7 @@
+// -- Setup.AllowedPublicKeys.iss --
+// The second key in this file should be replaced by your own and this will happen automatically when using build.bat or build-ce.bat
+// To ignore this change consider using Git's assume-unchanged or skip-worktree functionality
+//
+[ISSigKeys]
+Name: mykey1; KeyID: def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38; PublicX: e3e943066aff8f28d2219fd71c9ffff4c8d1aa26bc4225434be67180ab5e242d; PublicY: e419041c3f54551e86a1c47f387005cd535dfc9d64339b30d37f9a4f7866b650
+Name: mykey2; KeyID: def020edee3c4835fd54d85eff8b66d4d899b22a777353ca4a114b652e5e7a28; PublicX: 515dc7d6c16d4a46272ceb3d158c5630a96466ab4d948e72c2029d737c823097; PublicY: f3c21f6b5156c52a35f6f28016ee3e31a3ded60c325b81fb7b1f88c221081a61

+ 17 - 15
setup.iss

@@ -13,7 +13,7 @@
 [Setup]
 AppName=Inno Setup
 AppId={code:GetAppId|Inno Setup 6}
-AppVersion=6.4.3
+AppVersion=6.5.0-dev
 AppPublisher=jrsoftware.org
 AppPublisherURL=https://www.innosetup.com/
 AppSupportURL=https://www.innosetup.com/
@@ -112,6 +112,8 @@ Type: files; Name: "{app}\WizModernSmallImage-IS.bmp"
 ; Remove old ISCrypt.dll
 Type: files; Name: "{app}\ISCrypt.dll"
 
+#include "setup.allowedpublickeys.iss"
+
 #ifdef SIGNTOOL
   #define signcheck "signcheck"
 #else
@@ -123,10 +125,10 @@ Source: "license.txt"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\ISetup.chm"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\ISetup-dark.chm"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\Compil32.exe"; DestDir: "{app}"; Flags: ignoreversion signonce touch
-Source: "files\isscint.dll"; DestDir: "{app}"; Flags: ignoreversion signcheck touch
+Source: "files\isscint.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
 Source: "files\isscint.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\ISCC.exe"; DestDir: "{app}"; Flags: ignoreversion {#signcheck} touch
-Source: "files\ISCmplr.dll"; DestDir: "{app}"; Flags: ignoreversion {#signcheck} touch
+Source: "files\ISCmplr.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify {#signcheck} touch
 Source: "files\ISCmplr.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\Setup.e32"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\SetupLdr.e32"; DestDir: "{app}"; Flags: ignoreversion touch
@@ -137,17 +139,17 @@ Source: "files\WizClassicImage.bmp"; DestDir: "{app}"; Flags: ignoreversion touc
 Source: "files\WizClassicImage-IS.bmp"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\WizClassicSmallImage.bmp"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\WizClassicSmallImage-IS.bmp"; DestDir: "{app}"; Flags: ignoreversion touch
-Source: "files\iszlib.dll"; DestDir: "{app}"; Flags: ignoreversion signcheck touch
+Source: "files\iszlib.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
 Source: "files\iszlib.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
-Source: "files\isunzlib.dll"; DestDir: "{app}"; Flags: ignoreversion signonce touch
-Source: "files\isbzip.dll"; DestDir: "{app}"; Flags: ignoreversion signcheck touch
+Source: "files\isunzlib.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
+Source: "files\isbzip.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
 Source: "files\isbzip.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
-Source: "files\isbunzip.dll"; DestDir: "{app}"; Flags: ignoreversion signonce touch
-Source: "files\islzma.dll"; DestDir: "{app}"; Flags: ignoreversion signcheck touch
+Source: "files\isbunzip.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
+Source: "files\islzma.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
 Source: "files\islzma.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
-Source: "files\islzma32.exe"; DestDir: "{app}"; Flags: ignoreversion signonce touch
-Source: "files\islzma64.exe"; DestDir: "{app}"; Flags: ignoreversion signonce touch
-Source: "files\ISPP.dll"; DestDir: "{app}"; Flags: ignoreversion {#signcheck} touch
+Source: "files\islzma32.exe"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
+Source: "files\islzma64.exe"; DestDir: "{app}"; Flags: ignoreversion issigverify signcheck touch
+Source: "files\ISPP.dll"; DestDir: "{app}"; Flags: ignoreversion issigverify {#signcheck} touch
 Source: "files\ISPP.dll.issig"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\ISPPBuiltins.iss"; DestDir: "{app}"; Flags: ignoreversion touch
 Source: "files\ISSigTool.exe"; DestDir: "{app}"; Flags: ignoreversion signonce touch
@@ -172,11 +174,11 @@ Source: "Examples\ISPPExample1.iss"; DestDir: "{app}\Examples"; Flags: ignorever
 Source: "Examples\ISPPExample1License.txt"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\License.txt"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Languages.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
-Source: "Examples\MyDll.dll"; DestDir: "{app}\Examples"; Flags: ignoreversion signonce touch
+Source: "Examples\MyDll.dll"; DestDir: "{app}\Examples"; Flags: ignoreversion issigverify signcheck touch
 Source: "Examples\MyProg.chm"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
-Source: "Examples\MyProg.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion signonce touch
-Source: "Examples\MyProg-Arm64.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion signonce touch
-Source: "Examples\MyProg-x64.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion signonce touch
+Source: "Examples\MyProg.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion issigverify signcheck touch
+Source: "Examples\MyProg-Arm64.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion issigverify signcheck touch
+Source: "Examples\MyProg-x64.exe"; DestDir: "{app}\Examples"; Flags: ignoreversion issigverify signcheck touch
 Source: "Examples\PowerShell.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Readme.txt"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Readme-Dutch.txt"; DestDir: "{app}\Examples"; Flags: ignoreversion touch

+ 59 - 130
whatsnew.htm

@@ -29,145 +29,74 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
 
 <p><b>Want to be notified by e-mail of new Inno Setup releases?</b> <a href="ismail.php">Subscribe</a> to the Inno Setup Mailing List!</p>
 
-<p><a name="6.4.3"></a><span class="ver">6.4.3 </span><span class="date">(2025-05-03)</span></p>
+<p><a name="6.5.0"></a><span class="ver">6.5.0-dev </span><span class="date">(?)</span></p>
+<span class="head2">New signature-verification capability</span>
+<p>Inno Setup now includes an integrated signature-verification capability that can be used to detect corruption or tampering in files at compile time, before files are included in an installer being built, or during installation, before Setup copies external files onto a user's system.</p>
+<p>Any type of file may be signed and verified and creation of signatures does <i>not</i> require a certificate from a certificate authority. There is no cost involved.</p>
+<p>Note, however, that these signatures cannot be used to eliminate an "Unknown publisher" warning message shown by Windows when an installer or other EXE file is started. That requires a completely different kind of signature (Authenticode) embedded inside the EXE file by a different tool (Microsoft's <tt>signtool.exe</tt>), and it does require a (usually expensive) code-signing certificate from a certificate authority.</p>
+<p>A more detailed summary:</p>
 <ul>
-  <li>Compiler IDE change: The editor now restores selections on undo and redo.</li>
-  <li>Inno Setup now includes a new command-line tool, ISSigTool.exe. This tool is designed to sign files using ECDSA P-256 cryptographic signatures. Compil32, ISCC, and ISCmplr use these signatures to verify a number of DLL files before loading them. This is a &quot;technology preview&quot; that is subject to change (e.g., any signatures you create now may have to be recreated when upgrading to the next version).<br />Note: ISSigTool.exe does not replace Microsoft's signtool.exe in any way and is in fact not related to Authenticode Code Signing at all.</li>
-  <li><i>Fix:</i> Autocomplete support for parameter names in the <tt>[Components]</tt> and <tt>[Dirs]</tt> sections was broken since 6.1.1.</li>
-  <li><i>Fix:</i> Pascal Scripting support function <tt>Extract7ZipArchive</tt> which was introduced by 6.4.0 would fail with error 11 on older versions of Windows, at least Windows 8.1 and Windows Server 2016.</li>
-  <li>The maximum width of all task dialogs was increased by about 50%, which helps to keep long paths from being truncated with ellipses. This was done by setting Windows' <tt>TDF_SIZE_TO_CONTENT</tt> flag.</li>
-  <li>Minor tweaks and documentation improvements.</li>
-</ul>
-
-<p><a name="6.4.2"></a><span class="ver">6.4.2 </span><span class="date">(2025-03-12)</span></p>
-<ul>
-  <li>Added <tt>[Setup]</tt> section directive <tt>CloseApplicationsFilterExcludes</tt>.</li>
-  <li>Inno Setup is now built using Delphi 12.1 Athens instead of Delphi 11.3 Alexandria.</li>
-  <li>Inno Setup is now signed using a new code signing certificate.</li>
-</ul>
-
-<p><a name="6.4.1"></a><span class="ver">6.4.1 </span><span class="date">(2025-02-12)</span></p>
-<p><span class="head2">Compiler IDE changes</span></p>
-<ul>
-  <li>Added mouseover tooltips for all Pascal Scripting support functions and class members showing their prototype. Always shows all classes' members instead of just those of the current object's class.</li>
-  <li>Autocompletion lists now use the same font as the editor.</li>
-  <li><i>Fix:</i> When the IDE was started for the first time on a system with a DPI setting over 100%, the editor's initial font size was larger than expected.</li>
-</ul>
-<p><span class="head2">Other changes</span></p>
-<ul>
-  <li>{reg:...} constants can now also embed REG_DWORD-type registry values.</li>
-  <li><i>Fix:</i> In 6.4.0, using &quot;Shift+&quot; in a <tt>HotKey</tt> parameter in the [Icons] section didn't work and required &quot;Shift&quot; instead, so for example &quot;Ctrl+ShiftM&quot; instead of &quot;Ctrl+Shift+M&quot;.</li>
-  <li><i>Fix:</i> In 6.4.0, a custom form shown using <tt>[Code]</tt> didn't have a taskbar button if Setup's wizard was not visible at the time.</li>
-  <li>Added official Arabic translation.</li>
-  <li>Some minor tweaks and improvements.</li>
-</ul>
-
-<p><a name="6.4.0"></a><span class="ver">6.4.0 </span><span class="date">(2025-01-09)</span></p>
-<p><span class="head2">Compiler IDE changes</span></p>
-<p>Updated the Scintilla editor component used by the Compiler IDE to the latest version.</p>
-<p>Multiple selection editing has been improved:</p>
-<ul>
-  <li>Added new <i>Add Next Occurrence</i> menu item to the <i>Edit</i> menu to add the next occurrence of the current word or selected text as an additional selection (Shift+Alt+. or Ctrl+D, see below).</li>
-  <li>Added new <i>Select All Occurrences</i> menu item to the <i>Edit</i> menu to select all occurrences of the current word or selected text (Shift+Alt+; or Ctrl+Shift+L).</li>
-  <li>Added new <i>Select All Find Matches</i> menu item to the <i>Edit</i> menu to select all matches of the last find text (Alt+Enter).<br />Additionally, the <i>Find</i> (Ctrl+F) and <i>Replace</i> (Ctrl+H) dialogs now both support being closed by Alt+Enter to directly select all matches.</li>
-  <li>Added shortcuts to add a cursor or selection up or down (Ctrl+Alt+Up and Ctrl+Alt+Down). For multi-line selections this extends the selection up or down and never shrinks.</li>
-  <li>Added shortcut to add cursors to line ends (Shift+Alt+I). Behaves the same as in Visual Studio Code, so for example does nothing if all selections are empty.</li>
-  <li>Added shortcuts to add a word or line as an additional selection (Ctrl+Double Click and Ctrl+Triple Click or Alt+Double Click and Alt+Triple Click).</li>
-  <li>Added shortcut to remove a selection by clicking it (Ctrl+Click or Alt+Click).</li>
-  <li>Multiple selection now works over Left, Right, Up, Down, Home and End navigation and selection commands.</li>
-  <li>Multiple selection now works over word and line deletion commands, and line end insertion.</li>
-  <li>Multiple selection now works better with Copy and Paste commands.</li>
-  <li>Left, Right, etc. navigation with rectangular selection is now allowed.</li>
-  <li>The Find and Replace dialogs and the tools from the <i>Tools</i> menu which generate script text now all work better with multiple selections present.</li>
-</ul>
-<p>Other editor changes:</p>
-<ul>
-  <li>Added support for Visual Studio Code-style editor shortcuts, like Ctrl+D to Add Next Occurrence, Ctrl+Shift+K to delete a line and Alt+Click to add an additional cursor or remove a selection.<br />To activate this use the <i>Options</i> menu item in the <i>Tools</i> menu to set the new <i>Keys</i> option in the <i>Editor</i> group to <i>Visual Studio Code</i>.<br />The updated <a href="https://jrsoftware.org/ishelp/index.php?topic=compformshortcuts">Compiler IDE Keyboard And Mouse Commands</a> help topic lists all differences with the classic keyboard and mouse shortcuts.</li>
-  <li>Only if Visual Studio Code-style editor shortcuts have been activated: Added shortcuts to copy line down (Shift+Alt+Down) and to indent or unindent lines (Ctrl+] and Ctrl+[).</li>
-  <li>Added parameter hints for all Pascal Scripting support functions for quick reference to the function's parameter names, types, and order. Parameter hints can be invoked manually by pressing Ctrl+Shift+Space or automatically by using the new <i>Invoke parameter hints automatically</i> option which is enabled by default.</li>
-  <li>Added autocompletion support for all Pascal Scripting support functions, types, constants, etcetera. Existing option <i>Invoke autocompletion automatically</i> controls whether the autocompletion suggestions appear automatically or only when invoked manually by pressing Ctrl+Space or Ctrl+I.</li>
-  <li>Added parameter hints and autocompletion support for all Pascal Scripting support class members and properties. Both always show all classes' members and properties instead of just those of the current object's class.</li>
-  <li>Added autocompletion support for all Pascal Scripting event function parameters. Always shows all parameters instead of just those of the current event function.</li>
-  <li>Added autocompletion support for the [Messages] section.</li>
-  <li>Improved autocompletion support for all Flags parameters: now works for multiple flags instead of for the first only.</li>
-  <li>Added new <i>Enable section folding</i> option which allows you to temporarily hide sections while editing by clicking the new minus or plus icons in the editor's gutter or by using the new keyboard shortcuts (Ctrl+Shift+[ to fold and Ctrl+Shift+] to unfold) or menu items. Enabled by default.</li>
-  <li>The editor's gutter now shows change history to keep track of saved and unsaved modifications. Always enabled.</li>
-  <li>The editor's font now defaults to Consolas if available, consistent with most other modern editors.</li>
-  <li>The editor can now be scrolled horizontally instead of vertically by holding the Shift key while rotating the mouse wheel. Horizontal scroll wheels are now also supported.</li>
-  <li>Cut (Ctrl+X or Shift+Delete) and Copy (Ctrl+C or Ctrl+Insert) now cut or copy the entire line if there's no selection, consistent with most other modern editors.</li>
-  <li>Added new shortcuts to move selected lines up or down (Alt+Up and Alt+Down).</li>
-  <li>Added new shortcut and menu item to the <i>Edit</i> menu to toggle line comment (Ctrl+/).</li>
-  <li>Added new shortcut and menu item to the <i>Edit</i> menu to go to matching brace (Ctrl+Shift+\).</li>
-  <li>Moved the <i>Word Wrap</i> option to the <i>View</i> menu and added a shortcut for it (Alt+Z).</li>
-  <li>Added a right-click popup menu to the editor's gutter column for breakpoints.</li>
-  <li>Added dark mode support to autocompletion lists and also added a minimum width.</li>
-  <li>Added new <i>Show whitespace</i> option. Disabled by default.</li>
-  <li>Improved brace highlighting.</li>
-  <li>Fixed an issue when the <i>Auto indent mode</i> and <i>Allow cursor to move beyond end of lines</i> options are both enabled.</li>  
-</ul>
-<p>Other Compiler IDE changes:</p>
-<ul>
-  <li>Shortcuts Alt+Left and Alt+Right now always navigate back and forward even if Visual Studio-style menu shortcuts have been activated.<br />Because of this Alt+Right can no longer be used to initiate auto complete, instead the existing Ctrl+Space or Ctrl+I alternatives must be used.</li>
-  <li>Moved the list of recently opened files into a new <i>Open Recent</i> submenu of the <i>Files</i> menu.</li>
-  <li>Added new <i>Use Regular Expressions</i> option to the <i>Edit</i> menu to enable or disable the use of regular expressions for all find and replace operations and added a shortcut for it (Ctrl+Alt+R or Alt+R). Also added a small panel to the statusbar to indicate the current state.</li>
-  <li>The Find and Replace dialogs now support using Shift+Enter to temporarily search in the opposite direction.</li>
-  <li>Added shortcuts to select a tab (Ctrl+1 through Ctrl+9).</li>
-  <li>Added alternative shortcut for the <i>Compile</i> menu item in the <i>Build</i> menu (Shift+F9 or F7).</li>
-  <li>Added shortcut to the <i>Options</i> menu item in the <i>Tools</i> menu (Ctrl+,).</li>
-  <li>Removed the length limitation when entering a Sign Tool command and increased control height.</li>
-  <li>Added a banner which is displayed to each user after each update and links to this revision history.</li>
-  <li>Enabled dark mode support for the menus on Windows 11 Version 24H2 (2024 Update).</li>
-</ul>
-<p><span class="head2">Other changes</span></p>
-<ul>
-  <li>Updated the LZMA SDK used by Inno Setup to the latest version, increasing the speed of LZMA and LZMA2 compression (by 10% in a test with default settings) without changing the compression ratio. Compression memory requirements have increased by about 4%. This also made it possible to add support for extracting 7-Zip archives, see below.</li>
-  <li>Updated the encryption algorithm and key derivation function used by Inno Setup to XChaCha20 and PBKDF2-HMAC-SHA256 respectively, increasing security. This code is built-in: the separate ISCrypt.dll "encryption module" is no longer used and will be automatically deleted when you update.</li>
-  <li>Added <tt>[Setup]</tt> section directive <tt>EncryptionKeyDerivation</tt> to change the number of PBKDF2-HMAC-SHA256 iterations to use from the default of 200000 to another value.</li>
-  <li>Replaced all remaining use of MD5 and SHA-1 hashes with SHA-256 hashes, without removing the MD5 and SHA-1 Pascal Scripting and ISPP support functions.</li>
-  <li>At long last, Setup's wizard window now shows a thumbnail image on its taskbar button, and animates correctly when minimized and restored. As part of this work, support for the long-deprecated <tt>[Setup]</tt> section directive <tt>WindowVisible</tt>, which was used to enable a 1990s-style blue gradient background behind the wizard window, has been dropped. For the same reason Pascal Scripting support object <tt>MainForm</tt> has been removed.</li>
-  <li>The aspect ratio of Setup's large and small wizard images (as specified by <tt>WizardImageFile</tt> and <tt>WizardSmallImageFile</tt>) is now maintained when the window is scaled. Previously, depending on the font and font size used, they could have appeared horizontally stretched or squished.</li>
-  <li>The size of the small wizard image area has been extended to 58&times;58 (at standard DPI with the default font). Previous versions used a non-square 55&times;58 size, which made the default image look slightly stretched.</li>
-  <li>When disk spanning is enabled and Setup cannot find the needed disk slice file (e.g., <tt>setup-2.bin</tt>) in the source directory, it no longer automatically searches for it in a directory named <tt>DISKx</tt> one level up, where <tt>x</tt> is the disk number. Though consistent with old installers from the 16-bit era, this behavior wasn't documented.</li>
-  <li>The New Script Wizard now sets <tt>UninstallDisplayIcon</tt> when an .exe is chosen as the main executable file.</li>
-  <li>Merged the Inno Setup Preprocessor documentation into the main documentation instead of being separate.</li>
-  <li>Added a dark mode version of the documentation, automatically used by the Compiler IDE if a dark theme is chosen.</li>
-  <li>Pascal Scripting changes:
+  <li>New <tt>[ISSigKeys]</tt> section:
   <ul>
-    <li>Added new <tt>Extract7ZipArchive</tt> support function to extract a 7-Zip archive, based on the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov. See the new <a href="https://jrsoftware.org/ishelp/index.php?topic=isxfunc_extract7ziparchive">help topic</a> for information about its limitations.<br />Added new <tt>CreateExtractionPage</tt> support function to easily show the extraction progress to the user.</li>
-    <li>Added new <tt>ExecAndCaptureOutput</tt> support function to execute a program or batch file and capture its <i>stdout</i> and <i>stderr</i> outputs separately.</li>
-    <li>Added new <tt>StringJoin</tt>, <tt>StringSplit</tt>, and <tt>StringSplitEx</tt> support functions.</li>
-    <li>Output logging now raises an exception if there was an error setting up output redirection (which should be very rare). The <i>PowerShell.iss</i> example script has been updated to catch the exception.</li>
-    <li>Added new <tt>NewFolderName</tt> property to support class <tt>TInputDirWizardPage</tt> update the initial value passed to <tt>CreateInputDirPage</tt>.</li>
-    <li>Added new <tt>PopupMode</tt> and <tt>PopupParent</tt> properties to support class <tt>TForm</tt>.</li>
-    <li>Documented support functions <tt>VarArrayGet</tt> and <tt>VarArraySet</tt> which were already available but not documented.</li>
-    <li>Renamed the <tt>FileCopy</tt> support function to <tt>CopyFile</tt>. The old name is still supported, but it is recommended to update your scripts to the new name and the compiler will issue a warning if you don't.</li>
-    <li>Fixed support function <tt>TStream.CopyFrom</tt> by adding a <tt>BufferSize</tt> parameter which was required but missing. Using <tt>$100000</tt> as the value is recommended.</li>
-    <li>Condensed the logging of DLL function imports.</li>
-    <li>Added new <tt>Debugging</tt> support function.</li>
+    <li>Added a new optional <tt>[ISSigKeys]</tt> section for defining keys used by the compiler and Setup to verify file signatures.</li>
+    <li>Supports parameters <tt>Name</tt> (required) and <tt>Group</tt> to identify keys, parameters <tt>KeyFile</tt>, <tt>PublicX</tt>, and <tt>PublicY</tt> to specify the key values, and parameter <tt>KeyID</tt> to double-check the key values.</li>
+    <li>Key files are human-readable and can be created using Inno Setup Signature Tool (see below).</li>
+    <li>Example section:
+      <pre>[ISSigKeys]
+Name: anna: KeyFile: "anna.ispubkey"; Group: exesigner
+Name: ryan; KeyFile: "ryan.ispubkey"; Group: exesigner
+Name: ivan; KeyFile: "ivan.ispubkey"; Group: docsigner
+; max is trusted for both exe and doc signing
+Name: max; KeyFile: "max.ispubkey"; Group: exesigner docsigner
+; the boss has also has a key
+Name: bosskey; KeyFile: "boss.ispubkey"</pre>
+    </li>
   </ul>
   </li>
-  <li>ISPP change: Added support functions <tt>GetSHA256OfFile</tt>, <tt>GetSHA256OfString</tt>, and <tt>GetSHA256OfUnicodeString</tt>.</li>
-  <li>Inno Setup's Delphi <a href="https://github.com/jrsoftware/issrc" target="_blank">source code</a> has been reorganized to use unit scope names and additionally various units have been renamed for clarity. This makes it a lot easier to get started with working with the Inno Setup source code and making contributions, even with the free <a href="https://www.embarcadero.com/products/delphi/starter/free-download">Delphi Community Edition</a>.</li>
-  <li>Added official Swedish and Tamil translations.</li>
-  <li>Various tweaks and improvements.</li>
-</ul>
-
-<p>Contributions via <a href="https://github.com/jrsoftware/issrc" target="_blank">GitHub</a>: Thanks to Sergii Leonov, John Stevenson, and jogo- for their contributions!</p>
-
-<p>Thanks to Neil Hodgson and Igor Pavlov for their continued work on Scintilla and the LZMA SDK!</p>
-
-<p>Some messages have been added and changed in this version: (<a href="https://github.com/jrsoftware/issrc/commit/d18bab1c9234aa8bba6c8e277121e562724d331c">View differences in Default.isl</a>.)</p>
-<ul>
-  <li><b>New messages:</b>
+  <li>Updated <tt>[Files]</tt> section:
   <ul>
-    <li>ExtractionLabel, ButtonStopExtraction, StopExtraction, ErrorExtractionAborted, ErrorExtractionFailed.</li>
+    <li>Added a new <tt>issigverify</tt> flag for enforcing cryptographic signature verification of source files using a key from the <tt>[ISSigKeys]</tt> section, enhancing security during both compilation and installation.</li>
+    <li>When used without the <tt>external</tt> flag, verification is performed during compilation, aborting if it fails. When used with the <tt>external</tt> flag, verification occurs during installation, ensuring the integrity of files as they are copied.</li>
+    <li>Requires an <tt>.issig</tt> signature file to be present in the same directory as the source file. Signature files are human-readable files and can be created using the Inno Setup Signature Tool.</li>
+    <li>Has little performance impact since verification occurs while source files are being compressed/copied; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. This approach also ensures there is no Time-Of-Check to Time-Of-Use (TOCTOU) problem.</li>
+    <li>Can be used to verify downloaded files, offering flexibility over SHA-256 checks as script changes aren't needed for file updates. See the updated <i>CodeDownloadFiles.iss</i> example script for an example.</li>
+    <li>Added a new and optional <tt>ISSigAllowedKeys</tt> parameter to restrict which keys or groups of keys from the <tt>[ISSigKeys]</tt> section are permitted for signature verification using the <tt>issigverify</tt> flag.</li>
+    <li>Note: The <tt>issigverify</tt> flag cannot be combined with the <tt>sign</tt> or <tt>signonce</tt> flags. Use <tt>signcheck</tt> instead.</li>
+    <li>Example section:
+      <pre>[Files]
+Source: "MyProg.exe"; DestDir: "{app}"; Flags: issigverify; ISSigAllowedKeys: exesigner bosskey
+Source: "MyProg.chm"; DestDir: "{app}"; Flags: issigverify; ISSigAllowedKeys: docsigner bosskey</pre>
+    </li>
+  </ul>
+  </li>
+  <li>New Inno Setup Signature Tool:</li>
+  <ul>
+    <li>Added <tt>ISSigTool.exe</tt>, a new command-line utility designed to sign files using ECDSA P-256 cryptographic signatures.</li>
+    <li>Offers commands to sign and verify files, to export public keys and to generate private keys.</li>
+    <li>Example commands:
+      <pre>issigtool --key-file="MyKey.isprivatekey" generate-private-key
+issigtool --key-file="MyKey.isprivatekey" sign "MyProg.dll"
+issigtool --key-file="MyKey.isprivatekey" export-public-key "MyKey.ispublickey"
+issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"</pre>
+    </li>
+  </ul>
+  </li>
+  <li>New and updated documentation topics:</li>
+  <ul>
+    <li><a href="https://jrsoftware.org/ishelp/index.php?topic=issigkeyssection">[ISSigKeys] section</a></li>
+    <li><a href="https://jrsoftware.org/ishelp/index.php?topic=filessection">[Files] section</a></li>
+    <li><a href="https://jrsoftware.org/ishelp/index.php?topic=issig">.issig Signatures: Introduction</a></li>
+    <li><a href="https://jrsoftware.org/ishelp/index.php?topic=issigtool">Inno Setup Signature Tool</a></li>
+    <li>.issig File Format Specifications</li>
   </ul>
   </li>
 </ul>
+<span class="head2">Other changes</span>
+<ul>
+  <li>Example script <i>CodeDownloadFiles.iss</i> now also demonstrates how to use the <tt>CreateExtractionPage</tt> support function to extract a 7-Zip archive. See the <a href="https://jrsoftware.org/ishelp/index.php?topic=isxfunc_extract7ziparchive">Extract7ZipArchive help topic</a> for information about this function's limitations.</li>
+</ul>
 
-<p>Note: The official Icelandic translation has not yet been updated for these changes.</li>
-
-<p><a href="files/is6.3-whatsnew.htm">Inno Setup 6.3 Revision History</a></p>
+<p><a href="files/is6.4-whatsnew.htm">Inno Setup 6.4 Revision History</a></p>
 
 </body>
 </html>