Explorar el Código

Merge branch 'main' into trustfunc

Martijn Laan hace 4 meses
padre
commit
16e52fbb57

+ 409 - 0
Components/ECDSA.pas

@@ -0,0 +1,409 @@
+unit ECDSA;
+
+{
+  Inno Setup
+  Copyright (C) 1997-2025 Jordan Russell
+  Portions by Martijn Laan
+  For conditions of distribution and use, see LICENSE.TXT.
+
+  ECDSA-P256 signing, verification, and key generation, based on CNG (BCrypt)
+}
+
+interface
+
+uses
+  Windows, SysUtils;
+
+type
+  TECDSAInt256 = array[0..31] of Byte;
+  TECDSAPublicKey = packed record
+    Public_x: TECDSAInt256;
+    Public_y: TECDSAInt256;
+    procedure Clear;
+  end;
+  TECDSAPrivateKey = packed record
+    PublicKey: TECDSAPublicKey;
+    Private_d: TECDSAInt256;
+    procedure Clear;
+  end;
+  TECDSASignature = packed record
+    Sig_r: TECDSAInt256;
+    Sig_s: TECDSAInt256;
+    procedure Clear;
+  end;
+
+  TECDSAKey = class
+  private
+    FAlgorithmHandle: THandle;  { BCRYPT_ALG_HANDLE }
+    FKeyHandle: THandle;        { BCRYPT_KEY_HANDLE }
+    class procedure CheckStatus(const AFunctionName: String;
+      const AStatus: NTSTATUS); static;
+    procedure KeyHandleRequired;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure DestroyKey;
+    procedure ExportPrivateKey(out APrivateKey: TECDSAPrivateKey);
+    procedure ExportPublicKey(out APublicKey: TECDSAPublicKey);
+    procedure GenerateKeyPair;
+    procedure ImportPrivateKey([ref] const APrivateKey: TECDSAPrivateKey);
+    procedure ImportPublicKey([ref] const APublicKey: TECDSAPublicKey);
+    procedure SignHash(const AHash: array of Byte;
+      out ASignature: TECDSASignature);
+    function VerifySignature(const AHash: array of Byte;
+      const ASignature: TECDSASignature): Boolean;
+  end;
+
+  EECDSAError = class(Exception);
+
+implementation
+
+type
+  BCRYPT_ALG_HANDLE = type THandle;
+  BCRYPT_KEY_HANDLE = type THandle;
+
+  BCRYPT_ECCKEY_BLOB = record
+    dwMagic: ULONG;
+    cbKey: ULONG;
+  end;
+
+const
+  BCRYPT_ECDSA_P256_ALGORITHM = 'ECDSA_P256';
+  BCRYPT_ECCPRIVATE_BLOB = 'ECCPRIVATEBLOB';
+  BCRYPT_ECCPUBLIC_BLOB = 'ECCPUBLICBLOB';
+  BCRYPT_ECDSA_PRIVATE_P256_MAGIC = $32534345;
+  BCRYPT_ECDSA_PUBLIC_P256_MAGIC = $31534345;
+
+  STATUS_INVALID_SIGNATURE = NTSTATUS($C000A000);
+
+var
+  BCryptFunctionsInitialized: BOOL;
+
+  BCryptCloseAlgorithmProvider: function(hAlgorithm: BCRYPT_ALG_HANDLE;
+    dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptDestroyKey: function(hKey: BCRYPT_KEY_HANDLE): NTSTATUS;
+    stdcall;
+
+  BCryptExportKey: function(hKey: BCRYPT_KEY_HANDLE; hExportKey: BCRYPT_KEY_HANDLE;
+    pszBlobType: LPCWSTR; var pbOutput; cbOutput: ULONG; out pcbResult: ULONG;
+    dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptFinalizeKeyPair: function(hKey: BCRYPT_KEY_HANDLE; dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptGenerateKeyPair: function(hAlgorithm: BCRYPT_ALG_HANDLE;
+    out phKey: BCRYPT_KEY_HANDLE; dwLength: ULONG; dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptImportKeyPair: function(hAlgorithm: BCRYPT_ALG_HANDLE;
+    hImportKey: BCRYPT_KEY_HANDLE; pszBlobType: LPCWSTR;
+    out phKey: BCRYPT_KEY_HANDLE; const pbInput; cbInput: ULONG;
+    dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptOpenAlgorithmProvider: function(out phAlgorithm: BCRYPT_ALG_HANDLE;
+    pszAlgId: LPCWSTR; pszImplementation: LPCWSTR; dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptSignHash: function(hKey: BCRYPT_KEY_HANDLE; pPaddingInfo: Pointer;
+    const pbInput; cbInput: ULONG; var pbOutput; cbOutput: ULONG;
+    out pcbResult: ULONG; dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+  BCryptVerifySignature: function(hKey: BCRYPT_KEY_HANDLE; pPaddingInfo: Pointer;
+    const pbHash; cbHash: ULONG; const pbSignature; cbSignature: ULONG;
+    dwFlags: ULONG): NTSTATUS;
+    stdcall;
+
+type
+  { ECDSA-P256 key blob formats specific to BCrypt }
+  TBCryptPrivateKeyBlob = record
+    Header: BCRYPT_ECCKEY_BLOB;
+    Public_x: TECDSAInt256;
+    Public_y: TECDSAInt256;
+    Private_d: TECDSAInt256;
+    procedure Clear;
+  end;
+  TBCryptPublicKeyBlob = record
+    Header: BCRYPT_ECCKEY_BLOB;
+    Public_x: TECDSAInt256;
+    Public_y: TECDSAInt256;
+    procedure Clear;
+  end;
+
+procedure InitBCryptFunctions;
+var
+  M: HMODULE;
+
+  procedure InitFunc(out AProc: Pointer; const AProcName: PAnsiChar);
+  begin
+    AProc := GetProcAddress(M, AProcName);
+    if not Assigned(AProc) then
+      raise EECDSAError.CreateFmt('Failed to get address of %s in bcrypt.dll',
+        [AProcName]);
+  end;
+
+var
+  SystemDir: array[0..MAX_PATH-1] of Char;
+begin
+  if BCryptFunctionsInitialized then begin
+    MemoryBarrier;
+    Exit;
+  end;
+
+  GetSystemDirectory(SystemDir, SizeOf(SystemDir) div SizeOf(SystemDir[0]));
+  M := LoadLibrary(PChar(SystemDir + '\bcrypt.dll'));
+  if M = 0 then
+    raise EECDSAError.Create('Failed to load bcrypt.dll');
+
+  InitFunc(@BCryptCloseAlgorithmProvider, 'BCryptCloseAlgorithmProvider');
+  InitFunc(@BCryptDestroyKey, 'BCryptDestroyKey');
+  InitFunc(@BCryptExportKey, 'BCryptExportKey');
+  InitFunc(@BCryptFinalizeKeyPair, 'BCryptFinalizeKeyPair');
+  InitFunc(@BCryptGenerateKeyPair, 'BCryptGenerateKeyPair');
+  InitFunc(@BCryptImportKeyPair, 'BCryptImportKeyPair');
+  InitFunc(@BCryptOpenAlgorithmProvider, 'BCryptOpenAlgorithmProvider');
+  InitFunc(@BCryptSignHash, 'BCryptSignHash');
+  InitFunc(@BCryptVerifySignature, 'BCryptVerifySignature');
+
+  MemoryBarrier;
+  BCryptFunctionsInitialized := True;
+end;
+
+{ TBCryptPrivateKeyBlob }
+
+procedure TBCryptPrivateKeyBlob.Clear;
+begin
+  FillChar(Self, SizeOf(Self), 0);
+end;
+
+{ TBCryptPublicKeyBlob }
+
+procedure TBCryptPublicKeyBlob.Clear;
+begin
+  FillChar(Self, SizeOf(Self), 0);
+end;
+
+{ TECDSAPublicKey }
+
+procedure TECDSAPublicKey.Clear;
+begin
+  FillChar(Self, SizeOf(Self), 0);
+end;
+
+{ TECDSAPrivateKey }
+
+procedure TECDSAPrivateKey.Clear;
+begin
+  FillChar(Self, SizeOf(Self), 0);
+end;
+
+{ TECDSASignature }
+
+procedure TECDSASignature.Clear;
+begin
+  FillChar(Self, SizeOf(Self), 0);
+end;
+
+{ TECDSAKey }
+
+constructor TECDSAKey.Create;
+begin
+  inherited;
+  InitBCryptFunctions;
+  var LAlgorithmHandle: BCRYPT_ALG_HANDLE;
+  CheckStatus('BCryptOpenAlgorithmProvider',
+    BCryptOpenAlgorithmProvider(LAlgorithmHandle, BCRYPT_ECDSA_P256_ALGORITHM,
+      nil, 0));
+  FAlgorithmHandle := LAlgorithmHandle;  { assign only on success }
+end;
+
+destructor TECDSAKey.Destroy;
+begin
+  DestroyKey;
+  if FAlgorithmHandle <> 0 then
+    BCryptCloseAlgorithmProvider(FAlgorithmHandle, 0);
+  inherited;
+end;
+
+class procedure TECDSAKey.CheckStatus(const AFunctionName: String;
+  const AStatus: NTSTATUS);
+begin
+  if AStatus <> 0 then
+    raise EECDSAError.CreateFmt('%s failed with error code 0x%x',
+      [AFunctionName, AStatus]);
+end;
+
+procedure TECDSAKey.DestroyKey;
+begin
+  const H = FKeyHandle;
+  if H <> 0 then begin
+    FKeyHandle := 0;
+    BCryptDestroyKey(H);
+  end;
+end;
+
+procedure TECDSAKey.ExportPrivateKey(out APrivateKey: TECDSAPrivateKey);
+begin
+  KeyHandleRequired;
+
+  var KeyBlob: TBCryptPrivateKeyBlob;
+  { Initially clear KeyBlob just to make it easier to verify that
+    BCryptExportKey overwrites the entire record }
+  KeyBlob.Clear;
+  try
+    var ResultSize: ULONG;
+    CheckStatus('BCryptExportKey',
+      BCryptExportKey(FKeyHandle, 0, BCRYPT_ECCPRIVATE_BLOB, KeyBlob,
+        SizeOf(KeyBlob), ResultSize, 0));
+
+    if ResultSize <> SizeOf(KeyBlob) then
+      raise EECDSAError.Create('BCryptExportKey result invalid (1)');
+    if KeyBlob.Header.dwMagic <> BCRYPT_ECDSA_PRIVATE_P256_MAGIC then
+      raise EECDSAError.Create('BCryptExportKey result invalid (2)');
+    if KeyBlob.Header.cbKey <> SizeOf(KeyBlob.Public_x) then
+      raise EECDSAError.Create('BCryptExportKey result invalid (3)');
+
+    APrivateKey.PublicKey.Public_x := KeyBlob.Public_x;
+    APrivateKey.PublicKey.Public_y := KeyBlob.Public_y;
+    APrivateKey.Private_d := KeyBlob.Private_d;
+  finally
+    { Security: don't leave copy of private key on the stack }
+    KeyBlob.Clear;
+  end;
+end;
+
+procedure TECDSAKey.ExportPublicKey(out APublicKey: TECDSAPublicKey);
+begin
+  KeyHandleRequired;
+
+  var KeyBlob: TBCryptPublicKeyBlob;
+  { Initially clear KeyBlob just to make it easier to verify that
+    BCryptExportKey overwrites the entire record }
+  KeyBlob.Clear;
+  try
+    var ResultSize: ULONG;
+    CheckStatus('BCryptExportKey',
+      BCryptExportKey(FKeyHandle, 0, BCRYPT_ECCPUBLIC_BLOB, KeyBlob,
+        SizeOf(KeyBlob), ResultSize, 0));
+
+    if ResultSize <> SizeOf(KeyBlob) then
+      raise EECDSAError.Create('BCryptExportKey result invalid (1)');
+    if KeyBlob.Header.dwMagic <> BCRYPT_ECDSA_PUBLIC_P256_MAGIC then
+      raise EECDSAError.Create('BCryptExportKey result invalid (2)');
+    if KeyBlob.Header.cbKey <> SizeOf(KeyBlob.Public_x) then
+      raise EECDSAError.Create('BCryptExportKey result invalid (3)');
+
+    APublicKey.Public_x := KeyBlob.Public_x;
+    APublicKey.Public_y := KeyBlob.Public_y;
+  finally
+    { There's no private key, but clear anyway for consistency }
+    KeyBlob.Clear;
+  end;
+end;
+
+procedure TECDSAKey.GenerateKeyPair;
+begin
+  DestroyKey;
+
+  var LKeyHandle: BCRYPT_KEY_HANDLE;
+  CheckStatus('BCryptGenerateKeyPair',
+    BCryptGenerateKeyPair(FAlgorithmHandle, LKeyHandle, 256, 0));
+  try
+    CheckStatus('BCryptFinalizeKeyPair',
+      BCryptFinalizeKeyPair(LKeyHandle, 0));
+  except
+    BCryptDestroyKey(LKeyHandle);
+    raise;
+  end;
+  FKeyHandle := LKeyHandle;  { assign only on success }
+end;
+
+procedure TECDSAKey.ImportPrivateKey([ref] const APrivateKey: TECDSAPrivateKey);
+begin
+  DestroyKey;
+
+  var KeyBlob: TBCryptPrivateKeyBlob;
+  try
+    KeyBlob.Header.dwMagic := BCRYPT_ECDSA_PRIVATE_P256_MAGIC;
+    KeyBlob.Header.cbKey := SizeOf(KeyBlob.Public_x);
+    KeyBlob.Public_x := APrivateKey.PublicKey.Public_x;
+    KeyBlob.Public_y := APrivateKey.PublicKey.Public_y;
+    KeyBlob.Private_d := APrivateKey.Private_d;
+
+    var LKeyHandle: BCRYPT_KEY_HANDLE;
+    CheckStatus('BCryptImportKeyPair',
+      BCryptImportKeyPair(FAlgorithmHandle, 0, BCRYPT_ECCPRIVATE_BLOB,
+        LKeyHandle, KeyBlob, SizeOf(KeyBlob), 0));
+    FKeyHandle := LKeyHandle;  { assign only on success }
+  finally
+    { Security: don't leave copy of private key on the stack }
+    KeyBlob.Clear;
+  end;
+end;
+
+procedure TECDSAKey.ImportPublicKey([ref] const APublicKey: TECDSAPublicKey);
+begin
+  DestroyKey;
+
+  var KeyBlob: TBCryptPublicKeyBlob;
+  try
+    KeyBlob.Header.dwMagic := BCRYPT_ECDSA_PUBLIC_P256_MAGIC;
+    KeyBlob.Header.cbKey := SizeOf(KeyBlob.Public_x);
+    KeyBlob.Public_x := APublicKey.Public_x;
+    KeyBlob.Public_y := APublicKey.Public_y;
+
+    var LKeyHandle: BCRYPT_KEY_HANDLE;
+    CheckStatus('BCryptImportKeyPair',
+      BCryptImportKeyPair(FAlgorithmHandle, 0, BCRYPT_ECCPUBLIC_BLOB,
+        LKeyHandle, KeyBlob, SizeOf(KeyBlob), 0));
+    FKeyHandle := LKeyHandle;  { assign only on success }
+  finally
+    { There's no private key, but clear anyway for consistency }
+    KeyBlob.Clear;
+  end;
+end;
+
+procedure TECDSAKey.KeyHandleRequired;
+begin
+  if FKeyHandle = 0 then
+    raise EECDSAError.Create('No key has been assigned');
+end;
+
+procedure TECDSAKey.SignHash(const AHash: array of Byte;
+  out ASignature: TECDSASignature);
+begin
+  KeyHandleRequired;
+
+  { Initially clear ASignature just to make it easier to verify that
+    BCryptSignHash overwrites the entire record }
+  ASignature.Clear;
+
+  var ResultSize: ULONG;
+  CheckStatus('BCryptSignHash',
+    BCryptSignHash(FKeyHandle, nil, AHash[0], ULONG(Length(AHash)), ASignature,
+      SizeOf(ASignature), ResultSize, 0));
+
+  if ResultSize <> SizeOf(ASignature) then
+    raise EECDSAError.Create('BCryptSignHash result size invalid');
+end;
+
+function TECDSAKey.VerifySignature(const AHash: array of Byte;
+  const ASignature: TECDSASignature): Boolean;
+begin
+  KeyHandleRequired;
+
+  const Status = BCryptVerifySignature(FKeyHandle, nil, AHash[0],
+    ULONG(Length(AHash)), ASignature, SizeOf(ASignature), 0);
+  if Status = STATUS_INVALID_SIGNATURE then
+    Result := False
+  else begin
+    CheckStatus('BCryptVerifySignature', Status);
+    Result := True;
+  end;
+end;
+
+end.

+ 331 - 0
Components/ISSigFunc.pas

@@ -0,0 +1,331 @@
+unit ISSigFunc;
+
+{
+  Inno Setup
+  Copyright (C) 1997-2025 Jordan Russell
+  Portions by Martijn Laan
+  For conditions of distribution and use, see LICENSE.TXT.
+
+  Functions for creating/verifying .issig signatures and importing/exporting
+  text-based keys
+}
+
+interface
+
+uses
+  Windows, SysUtils, Classes, ECDSA, SHA256;
+
+type
+  TISSigVerifySignatureResult = (vsrSuccess, vsrMalformed, vsrKeyNotFound,
+    vsrBadSignature);
+  TISSigImportKeyResult = (ikrSuccess, ikrMalformed, ikrNotPrivateKey);
+
+{ Preferred, hardened functions for loading/saving .issig and key file text }
+function ISSigLoadTextFromFile(const AFilename: String): String;
+procedure ISSigSaveTextToFile(const AFilename, AText: String);
+
+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;
+
+procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
+  var APrivateKeyText: String);
+procedure ISSigExportPublicKeyText(const AKey: TECDSAKey;
+  var APublicKeyText: String);
+function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
+  const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
+
+function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
+
+implementation
+
+uses
+  StringScanner;
+
+const
+  ISSigTextFileLengthLimit = 500;
+
+  NonControlASCIICharsSet = [#32..#126];
+  DigitsSet = ['0'..'9'];
+  HexDigitsSet = DigitsSet + ['a'..'f'];
+
+function ECDSAInt256ToString(const Value: TECDSAInt256): String;
+begin
+  Result := SHA256DigestToString(TSHA256Digest(Value));
+end;
+
+function ECDSAInt256FromString(const S: String): TECDSAInt256;
+begin
+  TSHA256Digest(Result) := SHA256DigestFromString(S);
+end;
+
+function CalcHashToSign(const AFileSize: Int64;
+  const AFileHash: TSHA256Digest): TSHA256Digest;
+begin
+  var Context: TSHA256Context;
+  SHA256Init(Context);
+  SHA256Update(Context, AFileSize, SizeOf(AFileSize));
+  SHA256Update(Context, AFileHash, SizeOf(AFileHash));
+  Result := SHA256Final(Context);
+end;
+
+function CalcKeyID(const APublicKey: TECDSAPublicKey): TSHA256Digest;
+begin
+  Result := SHA256Buf(APublicKey, SizeOf(APublicKey));
+end;
+
+function ConsumeLineValue(var SS: TStringScanner; const AIdent: String;
+  var AValue: String; const AMinValueLength, AMaxValueLength: Integer;
+  const AAllowedChars: TSysCharSet): Boolean;
+begin
+  Result := False;
+  if SS.Consume(AIdent) and SS.Consume(' ') then
+    if SS.ConsumeMultiToString(AAllowedChars, AValue, AMinValueLength,
+       AMaxValueLength) > 0 then begin
+      { CRLF and LF line breaks are allowed (but not CR) }
+      SS.Consume(#13);
+      Result := SS.Consume(#10);
+    end;
+end;
+
+function ISSigLoadTextFromFile(const AFilename: String): String;
+{ Reads the specified file's contents into a string. This is intended only for
+  loading .issig and key files. If the file appears to be invalid (e.g., if
+  it is too large or contains invalid characters), then an empty string is
+  returned, which will be reported as malformed when it is processed by
+  ISSigVerifySignatureText or ISSigImportKeyText. }
+begin
+  var U: UTF8String;
+  SetLength(U, ISSigTextFileLengthLimit + 1);
+
+  const F = TFileStream.Create(AFilename, fmOpenRead or fmShareDenyWrite);
+  try
+    const BytesRead = F.Read(U[Low(U)], Length(U));
+    if BytesRead >= Length(U) then
+      Exit('');
+    SetLength(U, BytesRead);
+  finally
+    F.Free;
+  end;
+
+  { Defense-in-depth: Reject any non-CRLF control characters up front, as well
+    as any non-ASCII characters (to avoid any possible issues with converting
+    invalid multibyte characters) }
+  for var C in U do
+    if not CharInSet(C, [#10, #13, #32..#126]) then
+      Exit('');
+
+  Result := String(U);
+end;
+
+procedure ISSigSaveTextToFile(const AFilename, AText: String);
+begin
+  const F = TFileStream.Create(AFilename, fmCreate or fmShareExclusive);
+  try
+    const U = UTF8String(AText);
+    if U <> '' then
+      F.WriteBuffer(U[Low(U)], Length(U));
+  finally
+    F.Free;
+  end;
+end;
+
+function ISSigCreateSignatureText(const AKey: TECDSAKey;
+  const AFileSize: Int64; const AFileHash: TSHA256Digest): String;
+begin
+  { File size is limited to 16 digits (enough for >9 EB) }
+  if (AFileSize < 0) or (AFileSize > 9_999_999_999_999_999) then
+    raise Exception.Create('File size out of range');
+
+  var PublicKey: TECDSAPublicKey;
+  AKey.ExportPublicKey(PublicKey);
+
+  const HashToSign = CalcHashToSign(AFileSize, AFileHash);
+  var Sig: TECDSASignature;
+  AKey.SignHash(HashToSign, Sig);
+
+  Result := Format(
+    'format issig-v1'#13#10 +
+    'file-size %d'#13#10 +
+    'file-hash %s'#13#10 +
+    'key-id %s'#13#10 +
+    'sig-r %s'#13#10 +
+    'sig-s %s'#13#10,
+    [AFileSize,
+     SHA256DigestToString(AFileHash),
+     SHA256DigestToString(CalcKeyID(PublicKey)),
+     ECDSAInt256ToString(Sig.Sig_r),
+     ECDSAInt256ToString(Sig.Sig_s)]);
+end;
+
+function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
+  const AText: String; out AFileSize: Int64;
+  out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
+var
+  TextValues: record
+    Format, FileSize, FileHash, KeyID, Sig_r, Sig_s: String;
+  end;
+begin
+  { To be extra safe, clear the "out" parameters just in case the caller isn't
+    properly checking the function result }
+  AFileSize := -1;
+  FillChar(AFileHash, SizeOf(AFileHash), 0);
+
+  if Length(AText) > ISSigTextFileLengthLimit then
+    Exit(vsrMalformed);
+
+  var SS := TStringScanner.Create(AText);
+  if not ConsumeLineValue(SS, 'format', TextValues.Format, 8, 8, NonControlASCIICharsSet) or
+     (TextValues.Format <> 'issig-v1') or
+     not ConsumeLineValue(SS, 'file-size', TextValues.FileSize, 1, 16, DigitsSet) or
+     not ConsumeLineValue(SS, 'file-hash', TextValues.FileHash, 64, 64, HexDigitsSet) or
+     not ConsumeLineValue(SS, 'key-id', TextValues.KeyID, 64, 64, HexDigitsSet) or
+     not ConsumeLineValue(SS, 'sig-r', TextValues.Sig_r, 64, 64, HexDigitsSet) or
+     not ConsumeLineValue(SS, 'sig-s', TextValues.Sig_s, 64, 64, HexDigitsSet) or
+     not SS.ReachedEnd then
+    Exit(vsrMalformed);
+
+  { Don't allow leading zeros on file-size }
+  if (Length(TextValues.FileSize) > 1) and
+     (TextValues.FileSize[Low(TextValues.FileSize)] = '0') then
+    Exit(vsrMalformed);
+
+  { Find the key that matches the key ID }
+  var KeyUsed: TECDSAKey := nil;
+  const KeyID = SHA256DigestFromString(TextValues.KeyID);
+  for var K in AAllowedKeys do begin
+    var PublicKey: TECDSAPublicKey;
+    K.ExportPublicKey(PublicKey);
+    if SHA256DigestsEqual(KeyID, CalcKeyID(PublicKey)) then begin
+      KeyUsed := K;
+      Break;
+    end;
+  end;
+  if KeyUsed = nil then
+    Exit(vsrKeyNotFound);
+
+  const UnverifiedFileSize = StrToInt64(TextValues.FileSize);
+  const UnverifiedFileHash = SHA256DigestFromString(TextValues.FileHash);
+  const HashToSign = CalcHashToSign(UnverifiedFileSize, UnverifiedFileHash);
+  var Sig: TECDSASignature;
+  Sig.Sig_r := ECDSAInt256FromString(TextValues.Sig_r);
+  Sig.Sig_s := ECDSAInt256FromString(TextValues.Sig_s);
+  if KeyUsed.VerifySignature(HashToSign, Sig) then begin
+    AFileSize := UnverifiedFileSize;
+    AFileHash := UnverifiedFileHash;
+    Result := vsrSuccess;
+  end else
+    Result := vsrBadSignature;
+end;
+
+procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
+  var APrivateKeyText: String);
+begin
+  var PrivateKey: TECDSAPrivateKey;
+  try
+    AKey.ExportPrivateKey(PrivateKey);
+
+    APrivateKeyText := Format(
+      'format issig-private-key'#13#10 +
+      'key-id %s'#13#10 +
+      'public-x %s'#13#10 +
+      'public-y %s'#13#10 +
+      'private-d %s'#13#10,
+      [SHA256DigestToString(CalcKeyID(PrivateKey.PublicKey)),
+       ECDSAInt256ToString(PrivateKey.PublicKey.Public_x),
+       ECDSAInt256ToString(PrivateKey.PublicKey.Public_y),
+       ECDSAInt256ToString(PrivateKey.Private_d)]);
+  finally
+    PrivateKey.Clear;
+  end;
+end;
+
+procedure ISSigExportPublicKeyText(const AKey: TECDSAKey;
+  var APublicKeyText: String);
+begin
+  var PublicKey: TECDSAPublicKey;
+  try
+    AKey.ExportPublicKey(PublicKey);
+
+    APublicKeyText := Format(
+      'format issig-public-key'#13#10 +
+      'key-id %s'#13#10 +
+      'public-x %s'#13#10 +
+      'public-y %s'#13#10,
+      [SHA256DigestToString(CalcKeyID(PublicKey)),
+       ECDSAInt256ToString(PublicKey.Public_x),
+       ECDSAInt256ToString(PublicKey.Public_y)]);
+  finally
+    PublicKey.Clear;
+  end;
+end;
+
+function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
+  const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
+var
+  TextValues: record
+    Format, KeyID, Public_x, Public_y, Private_d: String;
+  end;
+begin
+  Result := ikrMalformed;
+  if Length(AText) > ISSigTextFileLengthLimit then
+    Exit;
+
+  var SS := TStringScanner.Create(AText);
+  if not ConsumeLineValue(SS, 'format', TextValues.Format, 16, 17, NonControlASCIICharsSet) then
+    Exit;
+  var HasPrivateKey := False;
+  if TextValues.Format = 'issig-private-key' then
+    HasPrivateKey := True
+  else if TextValues.Format = 'issig-public-key' then
+    { already False }
+  else
+    Exit;
+
+  if not ConsumeLineValue(SS, 'key-id', TextValues.KeyID, 64, 64, HexDigitsSet) or
+     not ConsumeLineValue(SS, 'public-x', TextValues.Public_x, 64, 64, HexDigitsSet) or
+     not ConsumeLineValue(SS, 'public-y', TextValues.Public_y, 64, 64, HexDigitsSet) then
+    Exit;
+  if HasPrivateKey then
+    if not ConsumeLineValue(SS, 'private-d', TextValues.Private_d, 64, 64, HexDigitsSet) then
+      Exit;
+  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);
+
+  { Verify that the key ID is correct for the public key values }
+  if not SHA256DigestsEqual(SHA256DigestFromString(TextValues.KeyID),
+     CalcKeyID(PrivateKey.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);
+  Result := ikrSuccess;
+end;
+
+function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
+var
+  Buf: array[0..$FFFF] of Byte;
+begin
+  var Context: TSHA256Context;
+  SHA256Init(Context);
+  while True do begin
+    const BytesRead = Cardinal(AStream.Read(Buf, SizeOf(Buf)));
+    if BytesRead = 0 then
+      Break;
+    SHA256Update(Context, Buf, BytesRead);
+  end;
+  Result := SHA256Final(Context);
+end;
+
+end.

+ 4 - 2
Components/PathFunc.pas

@@ -2,7 +2,7 @@ unit PathFunc;
 
 {
   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.
 
@@ -47,6 +47,8 @@ function RemoveBackslashUnlessRoot(const S: String): String;
 
 implementation
 
+{$ZEROBASEDSTRINGS OFF}
+
 uses
   Windows, SysUtils;
 
@@ -386,7 +388,7 @@ begin
   if S = '' then
     Result := nil
   else
-    Result := PathStrPrevChar(Pointer(S), @S[Length(S)+1]);
+    Result := @S[High(S)];
 end;
 
 function PathLastDelimiter(const Delimiters, S: string): Integer;

+ 24 - 1
Components/SHA256.pas

@@ -9,7 +9,7 @@ unit SHA256;
 interface
 
 uses
-  System.Hash;
+  SysUtils, System.Hash;
 
 type
   TSHA256Context = record
@@ -24,6 +24,7 @@ function SHA256Final(var ctx: TSHA256Context): TSHA256Digest;
 function SHA256Buf(const Buffer; Len: Cardinal): TSHA256Digest;
 function SHA256DigestsEqual(const A, B: TSHA256Digest): Boolean;
 function SHA256DigestToString(const D: TSHA256Digest): String;
+function SHA256DigestFromString(const S: String): TSHA256Digest;
 
 implementation
 
@@ -82,4 +83,26 @@ begin
   SetString(Result, Buf, 64);
 end;
 
+function SHA256DigestFromString(const S: String): TSHA256Digest;
+begin
+  if Length(S) <> 64 then
+    raise EConvertError.Create('Invalid string length');
+
+  for var I := 0 to 63 do begin
+    var B: Byte;
+    const C = UpCase(S.Chars[I]);
+    case C of
+      '0'..'9': B := Byte(Ord(C) - Ord('0'));
+      'A'..'F': B := Byte(Ord(C) - (Ord('A') - 10));
+    else
+      raise EConvertError.Create('Invalid digit character');
+    end;
+    const ResultIndex = I shr 1;
+    if not Odd(I) then
+      Result[ResultIndex] := Byte(B shl 4)
+    else
+      Result[ResultIndex] := Result[ResultIndex] or B;
+  end;
+end;
+
 end.

+ 123 - 0
Components/StringScanner.pas

@@ -0,0 +1,123 @@
+unit StringScanner;
+
+{
+  Inno Setup
+  Copyright (C) 1997-2025 Jordan Russell
+  Portions by Martijn Laan
+  For conditions of distribution and use, see LICENSE.TXT.
+
+  TStringScanner
+}
+
+interface
+
+uses
+  SysUtils;
+
+type
+  TStringScanner = record
+  strict private
+    FStr: String;
+    FPosition: Integer;
+    function GetReachedEnd: Boolean;
+    function GetRemainingCount: Integer;
+  public
+    class function Create(const AString: String): TStringScanner; static;
+    function Consume(const C: Char): Boolean; overload;
+    function Consume(const S: String): Boolean; overload;
+    function ConsumeMulti(const C: TSysCharSet; const AMinChars: Integer = 1;
+      const AMaxChars: Integer = Maxint): Integer;
+    function ConsumeMultiToString(const C: TSysCharSet;
+      var ACapturedString: String; const AMinChars: Integer = 1;
+      const AMaxChars: Integer = Maxint): Integer;
+    property ReachedEnd: Boolean read GetReachedEnd;
+    property RemainingCount: Integer read GetRemainingCount;
+    property Str: String read FStr;
+  end;
+
+implementation
+
+{$ZEROBASEDSTRINGS OFF}
+
+{ TStringScanner }
+
+class function TStringScanner.Create(const AString: String): TStringScanner;
+begin
+  Result.FPosition := 1;
+  Result.FStr := AString;
+end;
+
+function TStringScanner.Consume(const C: Char): Boolean;
+begin
+  Result := (GetRemainingCount > 0) and (FStr[FPosition] = C);
+  if Result then
+    Inc(FPosition);
+end;
+
+function TStringScanner.Consume(const S: String): Boolean;
+begin
+  const SLen = Length(S);
+  if SLen > GetRemainingCount then
+    Exit(False);
+
+  for var I := 0 to SLen-1 do
+    if FStr[FPosition + I] <> S[I+1] then
+      Exit(False);
+
+  Inc(FPosition, SLen);
+  Result := True;
+end;
+
+function TStringScanner.ConsumeMulti(const C: TSysCharSet;
+  const AMinChars: Integer = 1; const AMaxChars: Integer = Maxint): Integer;
+begin
+  if (AMinChars <= 0) or (AMinChars > AMaxChars) then
+    raise Exception.Create('TStringScanner.ConsumeMulti: Invalid parameter');
+
+  const Remain = GetRemainingCount;
+  if Remain < AMinChars then
+    Exit(0);
+
+  Result := 0;
+  while (Result < AMaxChars) and (Result < Remain) and
+     CharInSet(FStr[FPosition + Result], C) do
+    Inc(Result);
+
+  if Result < AMinChars then
+    Result := 0
+  else
+    Inc(FPosition, Result);
+end;
+
+function TStringScanner.ConsumeMultiToString(const C: TSysCharSet;
+  var ACapturedString: String; const AMinChars: Integer = 1;
+  const AMaxChars: Integer = Maxint): Integer;
+begin
+  const StartPos = FPosition;
+  Result := ConsumeMulti(C, AMinChars, AMaxChars);
+  if Result > 0 then
+    ACapturedString := Copy(FStr, StartPos, Result)
+  else
+    ACapturedString := '';
+end;
+
+function TStringScanner.GetReachedEnd: Boolean;
+begin
+  Result := (GetRemainingCount = 0);
+end;
+
+function TStringScanner.GetRemainingCount: Integer;
+begin
+  { The "<= 0" check exists to protect against OOB reads in case someone calls
+    into an instance that was never properly initialized (via Create).
+    Inside TStringScanner, FStr[FPosition] must not be accessed unless
+    GetRemainingCount is called first and returns a nonzero value. }
+
+  const Len = Length(FStr);
+  if (FPosition <= 0) or (FPosition > Len) then
+    Result := 0
+  else
+    Result := Len - FPosition + 1;
+end;
+
+end.

+ 277 - 0
Projects/ISSigTool.dpr

@@ -0,0 +1,277 @@
+program ISSigTool;
+
+{
+  Inno Setup
+  Copyright (C) 1997-2025 Jordan Russell
+  Portions by Martijn Laan
+  For conditions of distribution and use, see LICENSE.TXT.
+
+  "issigtool" utility
+}
+
+uses
+  SafeDLLPath in '..\Components\SafeDLLPath.pas',
+  SysUtils,
+  Classes,
+  PathFunc in '..\Components\PathFunc.pas',
+  SHA256 in '..\Components\SHA256.pas',
+  ECDSA in '..\Components\ECDSA.pas',
+  StringScanner in '..\Components\StringScanner.pas',
+  ISSigFunc in '..\Components\ISSigFunc.pas',
+  Shared.CommonFunc in 'Src\Shared.CommonFunc.pas',
+  Shared.FileClass in 'Src\Shared.FileClass.pas',
+  Shared.Int64Em in 'Src\Shared.Int64Em.pas';
+
+{$APPTYPE CONSOLE}
+{$SETPEOSVERSION 6.1}
+{$SETPESUBSYSVERSION 6.1}
+{$WEAKLINKRTTI ON}
+
+{$R Res\ISSigTool.manifest.res}
+{$R Res\ISSigTool.versionandicon.res}
+
+var
+  KeyFilename: String;
+
+procedure RaiseFatalError(const Msg: String);
+begin
+  raise Exception.Create(Msg);
+end;
+
+procedure RaiseFatalErrorFmt(const Msg: String; const Args: array of const);
+begin
+  raise Exception.CreateFmt(Msg, Args);
+end;
+
+function CalcFileHash(const AFile: TFile): TSHA256Digest;
+var
+  Buf: array[0..$FFFF] of Byte;
+begin
+  var Context: TSHA256Context;
+  SHA256Init(Context);
+  while True do begin
+    const BytesRead = AFile.Read(Buf, SizeOf(Buf));
+    if BytesRead = 0 then
+      Break;
+    SHA256Update(Context, Buf, BytesRead);
+  end;
+  Result := SHA256Final(Context);
+end;
+
+procedure CheckImportKeyResult(const AResult: TISSigImportKeyResult);
+begin
+  case AResult of
+    ikrSuccess:
+      Exit;
+    ikrMalformed:
+      RaiseFatalError('Key file is malformed');
+    ikrNotPrivateKey:
+      RaiseFatalError('Key file must be a private key when signing');
+  end;
+  RaiseFatalError('Unknown import key result');
+end;
+
+procedure CommandGeneratePrivateKey;
+begin
+  if NewFileExists(KeyFilename) then
+    RaiseFatalError('Key file already exists');
+
+  const Key = TECDSAKey.Create;
+  try
+    Key.GenerateKeyPair;
+
+    var PrivateKeyText: String;
+    ISSigExportPrivateKeyText(Key, PrivateKeyText);
+    ISSigSaveTextToFile(KeyFilename, PrivateKeyText);
+
+    Writeln(KeyFilename, ': OK');
+  finally
+    Key.Free;
+  end;
+end;
+
+procedure SignSingleFile(const AKey: TECDSAKey; const AFilename: String);
+begin
+  var FileSize: Int64;
+  var FileHash: TSHA256Digest;
+  const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
+  try
+    FileSize := Int64(F.Size);
+    FileHash := CalcFileHash(F);
+  finally
+    F.Free;
+  end;
+
+  const SigText = ISSigCreateSignatureText(AKey, FileSize, FileHash);
+  const ISSigFilename = AFilename + '.issig';
+  ISSigSaveTextToFile(ISSigFilename, SigText);
+
+   Writeln(ISSigFilename, ': OK')
+end;
+
+procedure CommandSign(const AFilenames: TStringList);
+begin
+  const Key = TECDSAKey.Create;
+  try
+    CheckImportKeyResult(ISSigImportKeyText(Key,
+      ISSigLoadTextFromFile(KeyFilename), True));
+
+    for var CurFilename in AFilenames do
+      SignSingleFile(Key, CurFilename);
+  finally
+    Key.Free;
+  end;
+end;
+
+function VerifySingleFile(const AKey: TECDSAKey; const AFilename: String): Boolean;
+begin
+  Result := False;
+  Write(AFilename, ': ');
+
+  if not NewFileExists(AFilename) then begin
+    Writeln('MISSINGFILE (File does not exist)');
+    Exit;
+  end;
+
+  const SigFilename = AFilename + '.issig';
+  if not NewFileExists(SigFilename) then begin
+    Writeln('MISSINGSIGFILE (Signature file does not exist)');
+    Exit;
+  end;
+
+  const SigText = ISSigLoadTextFromFile(SigFilename);
+  var ExpectedFileSize: Int64;
+  var ExpectedFileHash: TSHA256Digest;
+  const VerifyResult = ISSigVerifySignatureText([AKey], SigText,
+    ExpectedFileSize, ExpectedFileHash);
+  if VerifyResult <> vsrSuccess then begin
+    case VerifyResult of
+      vsrMalformed, vsrBadSignature:
+        Writeln('BADSIGFILE (Signature file is not valid)');
+      vsrKeyNotFound:
+        Writeln('UNKNOWNKEY (Incorrect key ID)');
+    else
+      RaiseFatalError('Unknown verify result');
+    end;
+    Exit;
+  end;
+
+  const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
+  try
+    if Int64(F.Size) <> ExpectedFileSize then begin
+      Writeln('WRONGSIZE (File size is incorrect)');
+      Exit;
+    end;
+    const ActualFileHash = CalcFileHash(F);
+    if not SHA256DigestsEqual(ActualFileHash, ExpectedFileHash) then begin
+      Writeln('WRONGHASH (File hash is incorrect)');
+      Exit;
+    end;
+  finally
+    F.Free;
+  end;
+
+  Writeln('OK');
+  Result := True;
+end;
+
+function CommandVerify(const AFilenames: TStringList): Boolean;
+begin
+  const Key = TECDSAKey.Create;
+  try
+    CheckImportKeyResult(ISSigImportKeyText(Key,
+      ISSigLoadTextFromFile(KeyFilename), False));
+
+    Result := True;
+    for var CurFilename in AFilenames do
+      if not VerifySingleFile(Key, CurFilename) then
+        Result := False;
+  finally
+    Key.Free;
+  end;
+end;
+
+procedure ShowBanner;
+begin
+  Writeln('Inno Setup Command-Line Signature Tool');
+  Writeln('Copyright (C) 1997-2025 Jordan Russell. All rights reserved.');
+  Writeln('Portions Copyright (C) 2000-2025 Martijn Laan. All rights reserved.');
+  Writeln('https://www.innosetup.com');
+  Writeln('');
+end;
+
+procedure ShowUsage;
+begin
+  Writeln(ErrOutput, 'Usage:  issigtool [options] sign <filename>');
+  Writeln(ErrOutput, 'or to verify:  issigtool [options] verify <filename>');
+  Writeln(ErrOutput, 'or to read generate private key:  issigtool [options] generate-private-key');
+  Writeln(ErrOutput, 'Options:');
+  Writeln(ErrOutput, '  --key-file=<filename> Specifies a key filename (overrides ISSIGTOOL_KEY_FILE environment variable)');
+  Writeln(ErrOutput, '');
+end;
+
+procedure Go;
+begin
+  const ArgList = TStringList.Create;
+  try
+    for var I := 1 to NewParamCount do
+      ArgList.Add(NewParamStr(I));
+
+    var J := 0;
+    while J < ArgList.Count do begin
+      const S = ArgList[J];
+      if S.StartsWith('--key-file=') then begin
+        KeyFilename := S.Substring(Length('--key-file='));
+        ArgList.Delete(J);
+      end else begin
+        if S.StartsWith('-') then
+          RaiseFatalErrorFmt('Unknown option "%s"', [S]);
+        if S = '' then
+          RaiseFatalError('Empty arguments not allowed');
+        Inc(J);
+      end;
+    end;
+
+    if ArgList.Count = 0 then begin
+      ShowUsage;
+      RaiseFatalError('Missing command argument');
+    end;
+    const Command = ArgList[0];
+    ArgList.Delete(0);
+
+    if KeyFilename = '' then begin
+      KeyFilename := GetEnv('ISSIGTOOL_KEY_FILE');
+      if KeyFilename = '' then
+        RaiseFatalError('"--key-file=" option must be specified, ' +
+          'or set the ISSIGTOOL_KEY_FILE environment variable');
+    end;
+
+    if Command = 'generate-private-key' then begin
+      if ArgList.Count <> 0 then
+        RaiseFatalError('Too many arguments');
+      CommandGeneratePrivateKey;
+    end else if Command = 'sign' then begin
+      if ArgList.Count = 0 then
+        RaiseFatalError('Missing filename argument');
+      CommandSign(ArgList);
+    end else if Command = 'verify' then begin
+      if ArgList.Count = 0 then
+        RaiseFatalError('Missing filename argument');
+      if not CommandVerify(ArgList) then
+        Halt(1);
+    end else
+      RaiseFatalErrorFmt('Unknown command "%s"', [Command]);
+  finally
+    ArgList.Free;
+  end;
+end;
+
+begin
+  try
+    ShowBanner;
+    Go;
+  except
+    Writeln(ErrOutput, 'issigtool fatal error: ', GetExceptMessage);
+    Halt(2);
+  end;
+end.

+ 114 - 0
Projects/ISSigTool.dproj

@@ -0,0 +1,114 @@
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+    <PropertyGroup>
+        <ProjectGuid>{484EE7D9-65D0-44DA-B807-9FC874733A64}</ProjectGuid>
+        <MainSource>ISSigTool.dpr</MainSource>
+        <Base>True</Base>
+        <Config Condition="'$(Config)'==''">Debug</Config>
+        <TargetedPlatforms>1</TargetedPlatforms>
+        <AppType>Console</AppType>
+        <FrameworkType>None</FrameworkType>
+        <ProjectVersion>20.1</ProjectVersion>
+        <Platform Condition="'$(Platform)'==''">Win32</Platform>
+        <ProjectName Condition="'$(ProjectName)'==''">ISSigTool</ProjectName>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
+        <Base>true</Base>
+    </PropertyGroup>
+    <PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
+        <Base_Win32>true</Base_Win32>
+        <CfgParent>Base</CfgParent>
+        <Base>true</Base>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
+        <Cfg_1>true</Cfg_1>
+        <CfgParent>Base</CfgParent>
+        <Base>true</Base>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
+        <Cfg_2>true</Cfg_2>
+        <CfgParent>Base</CfgParent>
+        <Base>true</Base>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Base)'!=''">
+        <DCC_ImageBase>00400000</DCC_ImageBase>
+        <DCC_AssertionsAtRuntime>false</DCC_AssertionsAtRuntime>
+        <DCC_DebugInformation>1</DCC_DebugInformation>
+        <DCC_SymbolReferenceInfo>1</DCC_SymbolReferenceInfo>
+        <DCC_ConsoleTarget>true</DCC_ConsoleTarget>
+        <DCC_UsePackage>VCL30;vclx30;VclSmp30;vcldb30;vcldbx30;$(DCC_UsePackage)</DCC_UsePackage>
+        <SanitizedProjectName>ISSigTool</SanitizedProjectName>
+        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
+        <VerInfo_MajorVer>0</VerInfo_MajorVer>
+        <VerInfo_Locale>1033</VerInfo_Locale>
+        <DCC_Namespace>System;System.Win;Winapi;$(DCC_Namespace)</DCC_Namespace>
+        <VerInfo_Keys>CompanyName=Jordan Russell;FileDescription=Inno Setup Command-Line Signature Tool;FileVersion=0.0.0.0;InternalName=;LegalCopyright=Copyright (C) 1997-2008 Jordan Russell. Portions Copyright (C) 2000-2008 Martijn Laan.;LegalTrademarks=;OriginalFilename=;ProductName=Inno Setup;ProductVersion=0.0.0.0;Comments=Inno Setup home page: http://www.innosetup.com</VerInfo_Keys>
+        <DCC_SYMBOL_DEPRECATED>false</DCC_SYMBOL_DEPRECATED>
+        <DCC_SYMBOL_PLATFORM>false</DCC_SYMBOL_PLATFORM>
+        <DCC_UNSAFE_CAST>false</DCC_UNSAFE_CAST>
+        <DCC_EXPLICIT_STRING_CAST>false</DCC_EXPLICIT_STRING_CAST>
+        <DCC_EXPLICIT_STRING_CAST_LOSS>false</DCC_EXPLICIT_STRING_CAST_LOSS>
+        <DCC_IMPLICIT_INTEGER_CAST_LOSS>false</DCC_IMPLICIT_INTEGER_CAST_LOSS>
+        <DCC_IMPLICIT_CONVERSION_LOSS>false</DCC_IMPLICIT_CONVERSION_LOSS>
+        <DCC_DcuOutput>Dcu\$(MainSource)</DCC_DcuOutput>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Base_Win32)'!=''">
+        <BT_BuildType>Debug</BT_BuildType>
+        <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
+        <VerInfo_Locale>1033</VerInfo_Locale>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Cfg_1)'!=''">
+        <DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
+        <DCC_DebugInformation>0</DCC_DebugInformation>
+        <DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
+        <DCC_ExeOutput>..\Files</DCC_ExeOutput>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Cfg_2)'!=''">
+        <DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
+        <DCC_Optimize>false</DCC_Optimize>
+        <DCC_GenerateStackFrames>true</DCC_GenerateStackFrames>
+        <DCC_ExeOutput>Bin</DCC_ExeOutput>
+    </PropertyGroup>
+    <ItemGroup>
+        <DelphiCompile Include="$(MainSource)">
+            <MainSource>MainSource</MainSource>
+        </DelphiCompile>
+        <DCCReference Include="..\Components\SafeDLLPath.pas"/>
+        <DCCReference Include="..\Components\PathFunc.pas"/>
+        <DCCReference Include="..\Components\SHA256.pas"/>
+        <DCCReference Include="..\Components\ECDSA.pas"/>
+        <DCCReference Include="..\Components\StringScanner.pas"/>
+        <DCCReference Include="..\Components\ISSigFunc.pas"/>
+        <DCCReference Include="Src\Shared.CommonFunc.pas"/>
+        <DCCReference Include="Src\Shared.FileClass.pas"/>
+        <DCCReference Include="Src\Shared.Int64Em.pas"/>
+        <BuildConfiguration Include="Base">
+            <Key>Base</Key>
+        </BuildConfiguration>
+        <BuildConfiguration Include="Release">
+            <Key>Cfg_1</Key>
+            <CfgParent>Base</CfgParent>
+        </BuildConfiguration>
+        <BuildConfiguration Include="Debug">
+            <Key>Cfg_2</Key>
+            <CfgParent>Base</CfgParent>
+        </BuildConfiguration>
+    </ItemGroup>
+    <ProjectExtensions>
+        <Borland.Personality>Delphi.Personality.12</Borland.Personality>
+        <Borland.ProjectType/>
+        <BorlandProject>
+            <Delphi.Personality>
+                <Source>
+                    <Source Name="MainSource">ISSigTool.dpr</Source>
+                </Source>
+            </Delphi.Personality>
+            <Platforms>
+                <Platform value="Win32">True</Platform>
+                <Platform value="Win64">False</Platform>
+            </Platforms>
+        </BorlandProject>
+        <ProjectFileVersion>12</ProjectFileVersion>
+    </ProjectExtensions>
+    <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
+    <Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
+</Project>

+ 15 - 3
Projects/Projects.groupproj

@@ -21,6 +21,9 @@
         <Projects Include="SetupLdr.dproj">
             <Dependencies/>
         </Projects>
+        <Projects Include="ISSigTool.dproj">
+            <Dependencies/>
+        </Projects>
         <Projects Include="..\ISHelp\ISHelpGen\ISHelpGen.dproj">
             <Dependencies/>
         </Projects>
@@ -86,6 +89,15 @@
     <Target Name="SetupLdr:Make">
         <MSBuild Projects="SetupLdr.dproj" Targets="Make"/>
     </Target>
+    <Target Name="ISSigTool">
+        <MSBuild Projects="ISSigTool.dproj"/>
+    </Target>
+    <Target Name="ISSigTool:Clean">
+        <MSBuild Projects="ISSigTool.dproj" Targets="Clean"/>
+    </Target>
+    <Target Name="ISSigTool:Make">
+        <MSBuild Projects="ISSigTool.dproj" Targets="Make"/>
+    </Target>
     <Target Name="ISHelpGen">
         <MSBuild Projects="..\ISHelp\ISHelpGen\ISHelpGen.dproj"/>
     </Target>
@@ -96,13 +108,13 @@
         <MSBuild Projects="..\ISHelp\ISHelpGen\ISHelpGen.dproj" Targets="Make"/>
     </Target>
     <Target Name="Build">
-        <CallTarget Targets="Compil32;ISCC;ISCmplr;ISPP;Setup;SetupLdr;ISHelpGen"/>
+        <CallTarget Targets="Compil32;ISCC;ISCmplr;ISPP;Setup;SetupLdr;ISSigTool;ISHelpGen"/>
     </Target>
     <Target Name="Clean">
-        <CallTarget Targets="Compil32:Clean;ISCC:Clean;ISCmplr:Clean;ISPP:Clean;Setup:Clean;SetupLdr:Clean;ISHelpGen:Clean"/>
+        <CallTarget Targets="Compil32:Clean;ISCC:Clean;ISCmplr:Clean;ISPP:Clean;Setup:Clean;SetupLdr:Clean;ISSigTool:Clean;ISHelpGen:Clean"/>
     </Target>
     <Target Name="Make">
-        <CallTarget Targets="Compil32:Make;ISCC:Make;ISCmplr:Make;ISPP:Make;Setup:Make;SetupLdr:Make;ISHelpGen:Make"/>
+        <CallTarget Targets="Compil32:Make;ISCC:Make;ISCmplr:Make;ISPP:Make;Setup:Make;SetupLdr:Make;ISSigTool:Make;ISHelpGen:Make"/>
     </Target>
     <Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
 </Project>

+ 1 - 0
Projects/Res/ISSigTool.manifest.rc

@@ -0,0 +1 @@
+1 24 ISSigTool.manifest.txt

BIN
Projects/Res/ISSigTool.manifest.res


+ 19 - 0
Projects/Res/ISSigTool.manifest.txt

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+    <security>
+        <requestedPrivileges>
+            <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
+        </requestedPrivileges>
+    </security>
+</trustInfo>
+<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+        <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+        <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+        <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+        <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+        <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+    </application>
+</compatibility>
+</assembly>

BIN
Projects/Res/ISSigTool.versionandicon.res


+ 3 - 1
build-ce.bat

@@ -10,7 +10,7 @@ rem
 rem  Calls setup-sign.bat if it exists, else creates setup.exe without signing
 rem
 rem  This batch files does the following things:
-rem  -Ask the user to compile Inno Setup and ISHelpGen after clearing output first
+rem  -Ask the user to compile Inno Setup including ISSigTool and ISHelpGen after clearing output first
 rem  -Compile ISetup*.chm
 rem  -Create Inno Setup installer
 rem
@@ -34,6 +34,7 @@ call :deletefile files\iscmplr.dll
 call :deletefile files\ispp.dll
 call :deletefile files\setup.e32
 call :deletefile files\setupldr.e32
+call :deletefile files\issigtool.exe
 call :deletefile ishelp\ishelpgen\ishelpgen.exe
 
 echo.
@@ -47,6 +48,7 @@ call :waitforfile files\iscmplr.dll
 call :waitforfile files\ispp.dll
 call :waitforfile files\setup.e32
 call :waitforfile files\setupldr.e32
+call :waitforfile files\issigtool.exe
 call :waitforfile ishelp\ishelpgen\ishelpgen.exe
 
 echo Found all, waiting 2 seconds more...

+ 1 - 1
build.bat

@@ -12,7 +12,7 @@ rem
 rem  This batch files does the following things:
 rem  -Compile ISHelpGen
 rem  -Compile ISetup*.chm
-rem  -Compile Inno Setup
+rem  -Compile Inno Setup including ISSigTool
 rem  -Create Inno Setup installer
 rem
 rem  Once done the installer can be found in Output

+ 5 - 0
compile.bat

@@ -65,6 +65,11 @@ mkdir Dcu\Setup.dpr 2>nul
 "%DELPHIXEROOT%\bin\dcc32.exe" --no-config -NSSystem;System.Win;Winapi;Vcl -Q -B -W %DELPHIXEDISABLEDWARNINGS% %1 -U"%DELPHIXEROOT%\lib\win32\release;..\Components\UniPs\Source" -E..\Files -NUDcu\Setup.dpr -DSETUPPROJ;PS_MINIVCL;PS_NOGRAPHCONST;PS_PANSICHAR;PS_NOINTERFACEGUIDBRACKETS Setup.dpr
 if errorlevel 1 goto failed
 
+echo - ISSigTool.dpr
+mkdir Dcu\ISSigTool.dpr 2>nul
+"%DELPHIXEROOT%\bin\dcc32.exe" --no-config -NSSystem;System.Win;Winapi -Q -B -H -W %DELPHIXEDISABLEDWARNINGS% %1 -U"%DELPHIXEROOT%\lib\win32\release" -E..\Files -NUDcu\ISSigTool.dpr ISSigTool.dpr
+if errorlevel 1 goto failed
+
 echo - Renaming E32 files
 cd ..\Files
 if errorlevel 1 goto failed