ISSigTool.dpr 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. program ISSigTool;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. "issigtool" utility
  8. }
  9. uses
  10. SafeDLLPath in '..\Components\SafeDLLPath.pas',
  11. SysUtils,
  12. Classes,
  13. PathFunc in '..\Components\PathFunc.pas',
  14. SHA256 in '..\Components\SHA256.pas',
  15. ECDSA in '..\Components\ECDSA.pas',
  16. StringScanner in '..\Components\StringScanner.pas',
  17. ISSigFunc in '..\Components\ISSigFunc.pas',
  18. Shared.CommonFunc in 'Src\Shared.CommonFunc.pas',
  19. Shared.FileClass in 'Src\Shared.FileClass.pas',
  20. Shared.Int64Em in 'Src\Shared.Int64Em.pas';
  21. {$APPTYPE CONSOLE}
  22. {$SETPEOSVERSION 6.1}
  23. {$SETPESUBSYSVERSION 6.1}
  24. {$WEAKLINKRTTI ON}
  25. {$R Res\ISSigTool.manifest.res}
  26. {$R Res\ISSigTool.versionandicon.res}
  27. var
  28. Options: record
  29. KeyFile: String;
  30. Quiet: Boolean;
  31. end;
  32. procedure RaiseFatalError(const Msg: String);
  33. begin
  34. raise Exception.Create(Msg);
  35. end;
  36. procedure RaiseFatalErrorFmt(const Msg: String; const Args: array of const);
  37. begin
  38. raise Exception.CreateFmt(Msg, Args);
  39. end;
  40. procedure Print(const S: String; const IncludeNewLine: Boolean = True);
  41. begin
  42. if IncludeNewLine then
  43. Writeln(S)
  44. else
  45. Write(S);
  46. end;
  47. procedure PrintUnlessQuiet(const S: String;
  48. const IncludeNewLine: Boolean = True);
  49. begin
  50. if not Options.Quiet then
  51. Print(S, IncludeNewLine);
  52. end;
  53. procedure PrintFmtUnlessQuiet(const S: String; const Args: array of const;
  54. const IncludeNewLine: Boolean = True);
  55. begin
  56. if not Options.Quiet then
  57. Print(Format(S, Args), IncludeNewLine);
  58. end;
  59. function CalcFileHash(const AFile: TFile): TSHA256Digest;
  60. var
  61. Buf: array[0..$FFFF] of Byte;
  62. begin
  63. var Context: TSHA256Context;
  64. SHA256Init(Context);
  65. while True do begin
  66. const BytesRead = AFile.Read(Buf, SizeOf(Buf));
  67. if BytesRead = 0 then
  68. Break;
  69. SHA256Update(Context, Buf, BytesRead);
  70. end;
  71. Result := SHA256Final(Context);
  72. end;
  73. procedure ImportKey(const AKey: TECDSAKey; const ANeedPrivateKey: Boolean);
  74. begin
  75. const ImportResult = ISSigImportKeyText(AKey,
  76. ISSigLoadTextFromFile(Options.KeyFile), ANeedPrivateKey);
  77. if ImportResult <> ikrSuccess then begin
  78. case ImportResult of
  79. ikrMalformed:
  80. RaiseFatalError('Key file is malformed');
  81. ikrNotPrivateKey:
  82. RaiseFatalError('Key file must be a private key when signing');
  83. end;
  84. RaiseFatalError('Unknown import key result');
  85. end;
  86. end;
  87. procedure CommandExportPublicKey(const AFilename: String);
  88. begin
  89. const Key = TECDSAKey.Create;
  90. try
  91. ImportKey(Key, False);
  92. var PublicKeyText: String;
  93. ISSigExportPublicKeyText(Key, PublicKeyText);
  94. ISSigSaveTextToFile(AFilename, PublicKeyText);
  95. PrintFmtUnlessQuiet('Public key file created: "%s"', [AFilename]);
  96. finally
  97. Key.Free;
  98. end;
  99. end;
  100. procedure CommandGeneratePrivateKey;
  101. begin
  102. if NewFileExists(Options.KeyFile) then
  103. RaiseFatalError('Key file already exists');
  104. const Key = TECDSAKey.Create;
  105. try
  106. Key.GenerateKeyPair;
  107. var PrivateKeyText: String;
  108. ISSigExportPrivateKeyText(Key, PrivateKeyText);
  109. ISSigSaveTextToFile(Options.KeyFile, PrivateKeyText);
  110. PrintFmtUnlessQuiet('Private key file created: "%s"', [Options.KeyFile]);
  111. finally
  112. Key.Free;
  113. end;
  114. end;
  115. procedure SignSingleFile(const AKey: TECDSAKey; const AFilename: String);
  116. begin
  117. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  118. var FileSize: Int64;
  119. var FileHash: TSHA256Digest;
  120. const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
  121. try
  122. FileSize := Int64(F.Size);
  123. FileHash := CalcFileHash(F);
  124. finally
  125. F.Free;
  126. end;
  127. const SigFilename = AFilename + '.issig';
  128. { ECDSA signature output is non-deterministic: signing the same hash with
  129. the same key produces a totally different signature each time. To avoid
  130. unnecessary alterations to the "sig-r" and "sig-s" values when a file is
  131. being re-signed but its contents haven't changed, we attempt to load and
  132. verify the existing .issig file. If the key, file size, and file hash are
  133. all up to date, then we skip creation of a new .issig file. }
  134. if NewFileExists(SigFilename) then begin
  135. const ExistingSigText = ISSigLoadTextFromFile(SigFilename);
  136. var ExistingFileSizeValue: Int64;
  137. var ExistingFileHashValue: TSHA256Digest;
  138. if ISSigVerifySignatureText([AKey], ExistingSigText, ExistingFileSizeValue,
  139. ExistingFileHashValue) = vsrSuccess then begin
  140. if (FileSize = ExistingFileSizeValue) and
  141. SHA256DigestsEqual(FileHash, ExistingFileHashValue) then begin
  142. PrintUnlessQuiet('signature unchanged');
  143. Exit;
  144. end;
  145. end;
  146. end;
  147. const SigText = ISSigCreateSignatureText(AKey, FileSize, FileHash);
  148. ISSigSaveTextToFile(SigFilename, SigText);
  149. PrintUnlessQuiet('signature written');
  150. end;
  151. procedure CommandSign(const AFilenames: TStringList);
  152. begin
  153. const Key = TECDSAKey.Create;
  154. try
  155. ImportKey(Key, True);
  156. for var CurFilename in AFilenames do
  157. SignSingleFile(Key, CurFilename);
  158. finally
  159. Key.Free;
  160. end;
  161. end;
  162. function VerifySingleFile(const AKey: TECDSAKey; const AFilename: String): Boolean;
  163. begin
  164. Result := False;
  165. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  166. if not NewFileExists(AFilename) then begin
  167. PrintUnlessQuiet('MISSINGFILE (File does not exist)');
  168. Exit;
  169. end;
  170. const SigFilename = AFilename + '.issig';
  171. if not NewFileExists(SigFilename) then begin
  172. PrintUnlessQuiet('MISSINGSIGFILE (Signature file does not exist)');
  173. Exit;
  174. end;
  175. const SigText = ISSigLoadTextFromFile(SigFilename);
  176. var ExpectedFileSize: Int64;
  177. var ExpectedFileHash: TSHA256Digest;
  178. const VerifyResult = ISSigVerifySignatureText([AKey], SigText,
  179. ExpectedFileSize, ExpectedFileHash);
  180. if VerifyResult <> vsrSuccess then begin
  181. case VerifyResult of
  182. vsrMalformed, vsrBadSignature:
  183. PrintUnlessQuiet('BADSIGFILE (Signature file is not valid)');
  184. vsrKeyNotFound:
  185. PrintUnlessQuiet('UNKNOWNKEY (Incorrect key ID)');
  186. else
  187. RaiseFatalError('Unknown verify result');
  188. end;
  189. Exit;
  190. end;
  191. const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
  192. try
  193. if Int64(F.Size) <> ExpectedFileSize then begin
  194. PrintUnlessQuiet('WRONGSIZE (File size is incorrect)');
  195. Exit;
  196. end;
  197. const ActualFileHash = CalcFileHash(F);
  198. if not SHA256DigestsEqual(ActualFileHash, ExpectedFileHash) then begin
  199. PrintUnlessQuiet('WRONGHASH (File hash is incorrect)');
  200. Exit;
  201. end;
  202. finally
  203. F.Free;
  204. end;
  205. PrintUnlessQuiet('OK');
  206. Result := True;
  207. end;
  208. function CommandVerify(const AFilenames: TStringList): Boolean;
  209. begin
  210. const Key = TECDSAKey.Create;
  211. try
  212. ImportKey(Key, False);
  213. Result := True;
  214. for var CurFilename in AFilenames do
  215. if not VerifySingleFile(Key, CurFilename) then
  216. Result := False;
  217. finally
  218. Key.Free;
  219. end;
  220. end;
  221. procedure ShowUsage;
  222. begin
  223. Writeln(ErrOutput, 'Inno Setup Signature Tool');
  224. Writeln(ErrOutput, 'Copyright (C) 1997-2025 Jordan Russell. All rights reserved.');
  225. Writeln(ErrOutput, 'Portions Copyright (C) 2000-2025 Martijn Laan. All rights reserved.');
  226. Writeln(ErrOutput, 'https://www.innosetup.com');
  227. Writeln(ErrOutput, '');
  228. Writeln(ErrOutput, 'Usage: issigtool [options] sign <filenames>');
  229. Writeln(ErrOutput, 'or to verify: issigtool [options] verify <filenames>');
  230. Writeln(ErrOutput, 'or to export the public key: issigtool [options] export-public-key <filename>');
  231. Writeln(ErrOutput, 'or to generate a new private key: issigtool [options] generate-private-key');
  232. Writeln(ErrOutput, 'Options:');
  233. Writeln(ErrOutput, ' --key-file=<filename> Specifies the private key filename (overrides ISSIGTOOL_KEY_FILE environment variable)');
  234. Writeln(ErrOutput, ' --quiet, -q Suppresses status messages that are normally printed to standard output');
  235. Writeln(ErrOutput, ' --help, -? Prints this information');
  236. Writeln(ErrOutput, '');
  237. end;
  238. procedure Go;
  239. begin
  240. const ArgList = TStringList.Create;
  241. try
  242. for var I := 1 to NewParamCount do
  243. ArgList.Add(NewParamStr(I));
  244. const InitialArgListCount = ArgList.Count;
  245. var J := 0;
  246. while J < ArgList.Count do begin
  247. const S = ArgList[J];
  248. if S.StartsWith('-') then begin
  249. if (S = '--help') or (S = '-?') then begin
  250. ShowUsage;
  251. if InitialArgListCount <> 1 then
  252. RaiseFatalErrorFmt('"%s" option cannot be combined with other arguments', [S]);
  253. Exit;
  254. end else if (S = '--quiet') or (S = '-q') then begin
  255. Options.Quiet := True;
  256. end else if S.StartsWith('--key-file=') then begin
  257. Options.KeyFile := S.Substring(Length('--key-file='));
  258. end else
  259. RaiseFatalErrorFmt('Unknown option "%s".', [S]);
  260. ArgList.Delete(J);
  261. end else begin
  262. if S = '' then
  263. RaiseFatalError('Empty arguments not allowed');
  264. Inc(J);
  265. end;
  266. end;
  267. if ArgList.Count = 0 then begin
  268. ShowUsage;
  269. RaiseFatalError('Missing command argument');
  270. end;
  271. const Command = ArgList[0];
  272. ArgList.Delete(0);
  273. if Options.KeyFile = '' then begin
  274. Options.KeyFile := GetEnv('ISSIGTOOL_KEY_FILE');
  275. if Options.KeyFile = '' then
  276. RaiseFatalError('"--key-file=" option must be specified, ' +
  277. 'or set the ISSIGTOOL_KEY_FILE environment variable');
  278. end;
  279. if Command = 'export-public-key' then begin
  280. if ArgList.Count = 0 then
  281. RaiseFatalError('Missing filename argument')
  282. else if ArgList.Count <> 1 then
  283. RaiseFatalError('Too many arguments');
  284. CommandExportPublicKey(ArgList[0]);
  285. end else if Command = 'generate-private-key' then begin
  286. if ArgList.Count <> 0 then
  287. RaiseFatalError('Too many arguments');
  288. CommandGeneratePrivateKey;
  289. end else if Command = 'sign' then begin
  290. if ArgList.Count = 0 then
  291. RaiseFatalError('Missing filename argument(s)');
  292. CommandSign(ArgList);
  293. end else if Command = 'verify' then begin
  294. if ArgList.Count = 0 then
  295. RaiseFatalError('Missing filename argument(s)');
  296. if not CommandVerify(ArgList) then
  297. Halt(1);
  298. end else
  299. RaiseFatalErrorFmt('Unknown command "%s"', [Command]);
  300. finally
  301. ArgList.Free;
  302. end;
  303. end;
  304. begin
  305. try
  306. Go;
  307. except
  308. Writeln(ErrOutput, 'issigtool fatal error: ', GetExceptMessage);
  309. Halt(2);
  310. end;
  311. end.