浏览代码

Merge branch 'main' into 7zdll

Martijn Laan 3 月之前
父节点
当前提交
a28a2f3366

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 /c.bat
 /compilesettings.bat
+/setup-presign.bat
 /setup-sign.bat
 /Output
 

+ 1 - 1
Components/NewUxTheme.pas

@@ -1103,7 +1103,7 @@ const
   themelib = 'uxtheme.dll';
 
 var
-  ThemeLibrary: THandle;
+  ThemeLibrary: HMODULE;
   ReferenceCount: Integer;  // We have to keep track of several load/unload calls.
 
 procedure FreeThemeLibrary;

+ 1 - 1
Components/RestartManager.pas

@@ -106,7 +106,7 @@ const
   restartmanagerlib = 'Rstrtmgr.dll';
 
 var
-  RestartManagerLibrary: THandle;
+  RestartManagerLibrary: HMODULE;
   ReferenceCount: Integer;  // We have to keep track of several load/unload calls.
 
 procedure FreeRestartManagerLibrary;

+ 2 - 0
Files/.gitignore

@@ -3,3 +3,5 @@
 *.e32
 *.dll
 *.exe
+/ISCmplr.dll.issig
+/ISPP.dll.issig

+ 4 - 3
ISHelp/.gitignore

@@ -1,3 +1,4 @@
-isxclasses_generated.xml
-isxfunc_generated.xml
-ispp_generated.xml
+/compilesettings.bat
+/isxclasses_generated.xml
+/isxfunc_generated.xml
+/ispp_generated.xml

+ 8 - 2
ISHelp/isetup.xml

@@ -2819,13 +2819,13 @@ Name: bosskey; KeyFile: "boss.ispublickey"
 
 <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>
+<p>Key names are not stored in the resulting Setup file(s), so you may use personal or non-public information in them, 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>
+<p>Group names are not stored in the resulting Setup file(s), so you may use personal or non-public information in them, like the names of developer groups.</p>
 <example><pre>Group: "exesigner docsigner"</pre></example>
 </param>
 
@@ -2853,6 +2853,12 @@ Name: bosskey; KeyFile: "boss.ispublickey"
 <example><pre>PublicY: "e419041c3f54551e86a1c47f387005cd535dfc9d64339b30d37f9a4f7866b650"</pre></example>
 </param>
 
+<param name="RuntimeID">
+<p>Specifies the runtime ID of the key, used by <link topic="isxfunc_ISSigVerify">ISSigVerify</link>.</p>
+<p>Runtime ID's are stored in the resulting Setup file(s), so you should <i>not</i> use personal or non-public information in them.</p>
+<example><pre>RuntimeID: "def01"</pre></example>
+</param>
+
 </paramlist>
 
 <heading>Remarks</heading>

+ 5 - 16
ISHelp/isxfunc.xml

@@ -1925,26 +1925,15 @@ end;</pre>
       </function>
     </subcategory>
     <subcategory>
-      <function>
-        <name>ISSigLoadTextFromFile</name>
-        <prototype>function ISSigLoadTextFromFile(const Filename: String): String;</prototype>
-        <description><p>Preferred, hardened function for loading specified Inno Setup Signature Tool key file text. Can also load .issig file text. An exception will be raised upon failure.</p></description>
-        <seealso><p><link topic="isxfunc_ISSigVerify">ISSigVerify</link></p></seealso>
-      </function>
       <function>
         <name>ISSigVerify</name>
-        <prototype>function ISSigVerify(const AllowedKeysTexts: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;</prototype>
-        <description><p>Verifies the signature of specified file using the specified key texts. An exception will be raised upon failure.</p>
-<p>Returns a handle to the still open file if True is specified in the KeepOpen parameter, <tt>nil</tt> otherwise. It is recommended that you always specify True if you plan to use the file for anything after verification. Otherwise, you risk creating a Time-Of-Check to Time-Of-Use (TOCTOU) problem.</p>
-<p>Key text from files should be loaded using <link topic="isxfunc_ISSigLoadTextFromFile">ISSigLoadTextFromFile</link>.</p></description>
+        <prototype>function ISSigVerify(const AllowedKeysRuntimeIDs: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;</prototype>
+        <description><p>Verifies the signature of the specified file using the specified allowed keys, looked up using [ISSigKeys] section parameter <tt>RuntimeID</tt>. If no keys are specified then all keys are allowed. An exception will be raised upon failure.</p>
+<p>Returns a handle to the still open file if True is specified in the KeepOpen parameter, <tt>nil</tt> otherwise. It is recommended that you always specify True if you plan to use the file for anything after verification. Otherwise, you risk creating a Time-Of-Check to Time-Of-Use (TOCTOU) problem.</p></description>
         <example><pre>var
-  AllowedKeyText: String;
-  AllowedKeysText: TArrayOfString;
+  F: TFileStream;
 begin
-  AllowedKeyText := ISSigLoadTextFromFile(KeyFilename);
-  SetLength(AllowedKeysText, 1);
-  AllowedKeysText[0] := AllowedKeyText;
-  F := ISSigVerify(AllowedKeysText, Filename, True);
+  F := ISSigVerify([], Filename, True);
   try
     // Use file
   finally

+ 1 - 1
Projects/Src/Compiler.Messages.pas

@@ -252,7 +252,7 @@ const
   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"';
+  SCompilerISSigKeysNameOrRuntimeIDExists = '%s "%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';

+ 20 - 4
Projects/Src/Compiler.SetupCompiler.pas

@@ -4501,8 +4501,18 @@ procedure TSetupCompiler.EnumISSigKeysProc(const Line: PChar; const Ext: Integer
     Result := False;
   end;
 
+  function ISSigKeysRuntimeIDExists(const RuntimeID: String): Boolean;
+  begin
+    for var I := 0 to ISSigKeyEntries.Count-1 do begin
+      var ISSigKeyEntry := PSetupISSigKeyEntry(ISSigKeyEntries[I]);
+      if SameText(ISSigKeyEntry.RuntimeID, RuntimeID) then
+        Exit(True)
+    end;
+    Result := False;
+  end;
+
 type
-  TParam = (paName, paGroup, paKeyFile, paKeyID, paPublicX, paPublicY);
+  TParam = (paName, paGroup, paKeyFile, paKeyID, paPublicX, paPublicY, paRuntimeID);
 const
   ParamISSigKeysName = 'Name';
   ParamISSigKeysGroup = 'Group';
@@ -4510,13 +4520,15 @@ const
   ParamISSigKeysKeyID = 'KeyID';
   ParamISSigKeysPublicX = 'PublicX';
   ParamISSigKeysPublicY = 'PublicY';
+  ParamISSigKeysRuntimeID = 'RuntimeID';
   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]));
+    (Name: ParamISSigKeysPublicY; Flags: [piNoEmpty]),
+    (Name: ParamISSigKeysRuntimeID; Flags: [piNoEmpty]));
 var
   Values: array[TParam] of TParamValue;
   NewISSigKeyEntry: PSetupISSigKeyEntry;
@@ -4534,7 +4546,7 @@ begin
       if not IsValidIdentString(Name, False, False) then
         AbortCompileFmt(SCompilerLanguagesOrISSigKeysBadName, [ParamISSigKeysName])
       else if ISSigKeysNameExists(Name, True) then
-        AbortCompileFmt(SCompilerISSigKeysNameExists, [Name]);
+        AbortCompileFmt(SCompilerISSigKeysNameOrRuntimeIDExists, [ParamISSigKeysName, Name]);
 
       { Group }
       var S := Values[paGroup].Data;
@@ -4545,7 +4557,7 @@ begin
         if not IsValidIdentString(GroupName, False, False) then
           AbortCompileFmt(SCompilerLanguagesOrISSigKeysBadGroupName, [ParamISSigKeysGroup])
         else if SameText(Name, GroupName) or ISSigKeysNameExists(GroupName, False) then
-          AbortCompileFmt(SCompilerISSigKeysNameExists, [GroupName]);
+          AbortCompileFmt(SCompilerISSigKeysNameOrRuntimeIDExists, [ParamISSigKeysName, GroupName]);
         if not HasGroupName(GroupName) then begin
           const N = Length(GroupNames);
           SetLength(GroupNames, N+1);
@@ -4604,6 +4616,10 @@ begin
         if not ISSigIsValidKeyIDForPublicXY(KeyID, PublicX, PublicY) then
           AbortCompile(SCompilerISSigKeysBadKeyID);
       end;
+
+      RuntimeID := Values[paRuntimeID].Data;
+      if ISSigKeysRuntimeIDExists(RuntimeID) then
+        AbortCompileFmt(SCompilerISSigKeysNameOrRuntimeIDExists, [ParamISSigKeysRuntimeID, RuntimeID]);
     end;
   except
     SEFreeRec(NewISSigKeyEntry, SetupISSigKeyEntryStrings, SetupISSigKeyEntryAnsiStrings);

+ 1 - 1
Projects/Src/IDE.ScintStylerInnoSetup.pas

@@ -235,7 +235,7 @@ const
   ];
 
   ISSigKeysSectionParameters: array of TScintRawString = [
-    'Name', 'Group', 'KeyFile', 'KeyID', 'PublicX', 'PublicY'
+    'Name', 'Group', 'KeyFile', 'KeyID', 'PublicX', 'PublicY', 'RuntimeID'
   ];
 
   FilesSectionParameters: array of TScintRawString = [

+ 1 - 13
Projects/Src/Setup.Install.pas

@@ -31,7 +31,7 @@ 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, ECDSA, ISSigFunc, Shared.CommonFunc.Vcl,
+  Compression.Base, SHA256, PathFunc, 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,
@@ -397,7 +397,6 @@ var
   UninstallDataCreated, UninstallMsgCreated, AppendUninstallData: Boolean;
   RegisterFilesList: TList;
   ExpandedAppId: String;
-  ISSigAvailableKeys: TArrayOfECDSAKey;
 
   function GetLocalTimeAsStr: String;
   var
@@ -3134,7 +3133,6 @@ begin
   AppendUninstallData := False;
   UninstLogCleared := False;
   RegisterFilesList := nil;
-  SetLength(ISSigAvailableKeys, 0);
   UninstLog := TSetupUninstallLog.Create;
   try
     try
@@ -3172,14 +3170,6 @@ 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;
@@ -3340,8 +3330,6 @@ 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]));

+ 14 - 1
Projects/Src/Setup.MainFunc.pas

@@ -111,6 +111,7 @@ var
   WizardImages: TList;
   WizardSmallImages: TList;
   CloseApplicationsFilterList, CloseApplicationsFilterExcludesList: TStringList;
+  ISSigAvailableKeys: TArrayOfECDSAKey;
 
   { User options }
   ActiveLanguage: Integer = -1;
@@ -236,7 +237,7 @@ function IsWindows11: Boolean;
 implementation
 
 uses
-  ShellAPI, ShlObj, StrUtils, ActiveX, RegStr, ChaCha20,
+  ShellAPI, ShlObj, StrUtils, ActiveX, RegStr, ChaCha20, ECDSA, ISSigFunc,
   SetupLdrAndSetup.Messages, Shared.SetupMessageIDs, Setup.Install, SetupLdrAndSetup.InstFunc,
   Setup.InstFunc, SetupLdrAndSetup.RedirFunc, PathFunc,
   Compression.Base, Compression.Zlib, Compression.bzlib, Compression.LZMADecompressor,
@@ -3228,6 +3229,15 @@ begin
   { Set install mode }
   SetupInstallMode;
 
+  { Init ISSigAvailableKeys }
+  SetLength(ISSigAvailableKeys, Entries[seISSigKey].Count);
+  for I := 0 to Entries[seISSigKey].Count-1 do begin
+    var ISSigKeyEntry := PSetupISSigKeyEntry(Entries[seISSigKey][I]);
+    ISSigAvailableKeys[I] := TECDSAKey.Create;
+    if ISSigImportPublicKey(ISSigAvailableKeys[I], '', ISSigKeyEntry.PublicX, ISSigKeyEntry.PublicY) <> ikrSuccess then
+      InternalError('ISSigImportPublicKey failed')
+  end;
+
   { Load and initialize code }
   if SetupHeader.CompiledCodeText <> '' then begin
     CodeRunner := TScriptRunner.Create();
@@ -3473,6 +3483,9 @@ begin
       DeleteDirsAfterInstallList[I]);
   DeleteDirsAfterInstallList.Clear;
 
+  for I := 0 to Length(ISSigAvailableKeys)-1 do
+    ISSigAvailableKeys[I].Free;
+
   FreeFileExtractor;
 
   { End RestartManager session }

+ 45 - 47
Projects/Src/Setup.ScriptFunc.pas

@@ -1816,61 +1816,59 @@ var
         Parts := Stack.GetString(PStart-1).Split(Separators, TStringSplitOptions(Stack.GetInt(PStart-3)));
       Stack.SetArray(PStart, Parts);
     end);
-    RegisterScriptFunc('ISSigLoadTextFromFile', procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
-    begin
-      Stack.SetString(PStart, ISSigLoadTextFromFile(Stack.GetString(PStart-1)));
-    end);
     RegisterScriptFunc('ISSigVerify', procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
     begin
-      const AllowedKeysTexts = Stack.GetStringArray(PStart-1);
+      const AllowedKeysRuntimeIDs = Stack.GetStringArray(PStart-1);
       const Filename = Stack.GetString(PStart-2);
       const KeepOpen = Stack.GetBool(PStart-3);
 
-      var ExpectedFileSize: Int64;
-      var ExpectedFileHash: TSHA256Digest;
-
-      var AllowedKeys: TArrayOfECDSAKey;
-      const NAllowedKeys = Length(AllowedKeysTexts);
-      SetLength(AllowedKeys, NAllowedKeys);
-      try
-        { Import keys }
-        for var I := 0 to NAllowedKeys-1 do begin
-          AllowedKeys[I] := TECDSAKey.Create;
-          const ImportResult = ISSigImportKeyText(AllowedKeys[I], AllowedKeysTexts[I], False);
-          if ImportResult = ikrMalformed then
-            InternalError('Key text is malformed')
-          else if ImportResult <> ikrSuccess then
-            InternalError('Unknown import key result');
+      { Import keys }
+      var ISSigAllowedKeys: AnsiString;
+      for var I := 0 to Length(AllowedKeysRuntimeIDs)-1 do begin
+        const RuntimeID = AllowedKeysRuntimeIDs[I];
+        if RuntimeID = '' then
+          InternalError('RuntimeID cannot be empty');
+        var Found := False;
+        for var KeyIndex := 0 to Entries[seISSigKey].Count-1 do begin
+          var ISSigKeyEntry := PSetupISSigKeyEntry(Entries[seISSigKey][KeyIndex]);
+          if SameText(ISSigKeyEntry.RuntimeID, RuntimeID) then begin
+            SetISSigAllowedKey(ISSigAllowedKeys, KeyIndex);
+            Found := True;
+            Break;
+          end;
         end;
-
-        { Verify signature }
-        if not ISSigVerifySignature(Filename, AllowedKeys, ExpectedFileSize, ExpectedFileHash,
-          procedure(const Filename: String)
-          begin
-            raise Exception.Create('File does not exist');
-          end,
-          procedure(const Filename, SigFilename: String)
-          begin
-            raise Exception.Create('Signature file does not exist');
-          end,
-          procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
-          begin
-            case VerifyResult of
-              vsrMalformed, vsrBadSignature:
-                raise Exception.Create('Signature file is not valid');
-              vsrKeyNotFound:
-                raise Exception.Create('Incorrect key ID');
-            else
-              InternalError('Unknown verify result');
-            end;
-          end
-        ) then
-          InternalError('Unexpected ISSigVerifySignature result');
-      finally
-        for var I := 0 to NAllowedKeys-1 do
-          AllowedKeys[I].Free;
+        if not Found then
+          InternalError(Format('Unknown RuntimeID ''%s''', [RuntimeID]));
       end;
 
+      { Verify signature }
+      var ExpectedFileSize: Int64;
+      var ExpectedFileHash: TSHA256Digest;
+      if not ISSigVerifySignature(Filename,
+        GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys),
+        ExpectedFileSize, ExpectedFileHash,
+        procedure(const Filename: String)
+        begin
+          raise Exception.Create('File does not exist');
+        end,
+        procedure(const Filename, SigFilename: String)
+        begin
+          raise Exception.Create('Signature file does not exist');
+        end,
+        procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
+        begin
+          case VerifyResult of
+            vsrMalformed, vsrBadSignature:
+              raise Exception.Create('Signature file is not valid');
+            vsrKeyNotFound:
+              raise Exception.Create('Incorrect key ID');
+          else
+            InternalError('Unknown verify result');
+          end;
+        end
+      ) then
+        InternalError('Unexpected ISSigVerifySignature result');
+
       { Verify file, keeping open afterwards if requested }
       var F := TFileStream.Create(Filename, fmOpenRead or fmShareDenyWrite);
       try

+ 1 - 2
Projects/Src/Shared.ScriptFunc.pas

@@ -546,8 +546,7 @@ initialization
     'function StringJoin(const Separator: String; const Values: TArrayOfString): String;',
     'function StringSplit(const S: String; const Separators: TArrayOfString; const Typ: TSplitType): TArrayOfString;',
     'function StringSplitEx(const S: String; const Separators: TArrayOfString; const Quote: Char; const Typ: TSplitType): TArrayOfString;',
-    'function ISSigLoadTextFromFile(const Filename: String): String;',
-    'function ISSigVerify(const AllowedKeysTexts: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;'
+    'function ISSigVerify(const AllowedKeysRuntimeIDs: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;'
   ];
 
   {$IFDEF COMPIL32PROJ}

+ 3 - 3
Projects/Src/Shared.SetupTypes.pas

@@ -312,11 +312,10 @@ 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);
+{ ISSigAllowedKeys should start out empty. If you then only use this function
+  to update it, regular string comparison can be used for comparisons. }
 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;
@@ -335,6 +334,7 @@ end;
 
 function GetISSigAllowedKeys([Ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
   const ISSigAllowedKeys: AnsiString): TArrayOfECDSAKey;
+{ Returns all keys if ISSigAllowedKeys is empty! }
 begin
   if ISSigAllowedKeys <> '' then begin
     const NAvailable = Length(ISSigAvailableKeys);

+ 2 - 2
Projects/Src/Shared.Struct.pas

@@ -218,12 +218,12 @@ type
       doUninsAlwaysUninstall, doSetNTFSCompression, doUnsetNTFSCompression);
   end;
 const
-  SetupISSigKeyEntryStrings = 2;
+  SetupISSigKeyEntryStrings = 3;
   SetupISSigKeyEntryAnsiStrings = 0;
 type
   PSetupISSigKeyEntry = ^TSetupISSigKeyEntry;
   TSetupISSigKeyEntry = packed record
-    PublicX, PublicY: String;
+    PublicX, PublicY, RuntimeID: String;
   end;
 const
   SetupFileEntryStrings = 11;

+ 2 - 0
issig.bat

@@ -23,6 +23,8 @@ echo compilesettings.bat is missing or incomplete. It needs to contain
 echo the following line, adjusted for your system:
 echo.
 echo   set ISSIGTOOL_KEY_FILE=x:\path\MyKey.isprivatekey
+echo.
+echo Keep the path outside of your source tree!
 goto failed2
 
 :compilesettingsfound

+ 1 - 1
whatsnew.htm

@@ -48,7 +48,7 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
   <li>New <tt>[ISSigKeys]</tt> section:
   <ul>
     <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>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, parameter <tt>KeyID</tt> to double-check the key values, and parameter <tt>RuntimeID</tt> for runtime identification of the key.</li>
     <li>Key files are human-readable and can be created using Inno Setup Signature Tool (see below).</li>
     <li>Example section:
       <pre>[ISSigKeys]