ISSigTool.dpr 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. Windows,
  14. PathFunc in '..\Components\PathFunc.pas',
  15. SHA256 in '..\Components\SHA256.pas',
  16. ECDSA in '..\Components\ECDSA.pas',
  17. StringScanner in '..\Components\StringScanner.pas',
  18. ISSigFunc in '..\Components\ISSigFunc.pas',
  19. Shared.CommonFunc in 'Src\Shared.CommonFunc.pas',
  20. Shared.FileClass in 'Src\Shared.FileClass.pas',
  21. UnsignedFunc in '..\Components\UnsignedFunc.pas';
  22. {$APPTYPE CONSOLE}
  23. {$SETPEOSVERSION 6.1}
  24. {$SETPESUBSYSVERSION 6.1}
  25. {$WEAKLINKRTTI ON}
  26. {$R Res\ISSigTool.manifest.res}
  27. {$R Res\ISSigTool.versionandicon.res}
  28. var
  29. Options: record
  30. KeyFile: String;
  31. AllowOverwrite, Quiet: Boolean;
  32. end;
  33. StdOutHandle, StdErrHandle: THandle;
  34. StdOutHandleIsConsole, StdErrHandleIsConsole: Boolean;
  35. procedure RaiseFatalError(const Msg: String);
  36. begin
  37. raise Exception.Create(Msg);
  38. end;
  39. procedure RaiseFatalErrorFmt(const Msg: String; const Args: array of const);
  40. begin
  41. raise Exception.CreateFmt(Msg, Args);
  42. end;
  43. procedure Print(const Handle: THandle; const HandleIsConsole: Boolean;
  44. S: String; const IncludeNewLine: Boolean); overload;
  45. begin
  46. if IncludeNewLine then
  47. S := S + #13#10;
  48. if HandleIsConsole then begin
  49. var CharsWritten: DWORD;
  50. WriteConsole(Handle, @S[1], ULength(S), CharsWritten, nil);
  51. end else begin
  52. var Utf8S := Utf8Encode(S);
  53. var BytesWritten: DWORD;
  54. WriteFile(Handle, Utf8S[1], ULength(Utf8S), BytesWritten, nil);
  55. end;
  56. end;
  57. procedure Print(const S: String; const IncludeNewLine: Boolean = True); overload;
  58. begin
  59. Print(StdOutHandle, StdOutHandleIsConsole, S, IncludeNewLine);
  60. end;
  61. procedure PrintErrOutput(const S: String; const IncludeNewLine: Boolean = True); overload;
  62. begin
  63. Print(StdErrHandle, StdErrHandleIsConsole, S, IncludeNewLine);
  64. end;
  65. procedure PrintUnlessQuiet(const S: String;
  66. const IncludeNewLine: Boolean = True);
  67. begin
  68. if not Options.Quiet then
  69. Print(S, IncludeNewLine);
  70. end;
  71. procedure PrintFmtUnlessQuiet(const S: String; const Args: array of const;
  72. const IncludeNewLine: Boolean = True);
  73. begin
  74. if not Options.Quiet then
  75. Print(Format(S, Args), IncludeNewLine);
  76. end;
  77. function CalcFileHash(const AFile: TFile): TSHA256Digest;
  78. var
  79. Buf: array[0..$FFFF] of Byte;
  80. begin
  81. var Context: TSHA256Context;
  82. SHA256Init(Context);
  83. while True do begin
  84. const BytesRead = AFile.Read(Buf, SizeOf(Buf));
  85. if BytesRead = 0 then
  86. Break;
  87. SHA256Update(Context, Buf, BytesRead);
  88. end;
  89. Result := SHA256Final(Context);
  90. end;
  91. procedure ImportKey(const AKey: TECDSAKey; const ANeedPrivateKey: Boolean);
  92. begin
  93. const ImportResult = ISSigImportKeyText(AKey,
  94. ISSigLoadTextFromFile(Options.KeyFile), ANeedPrivateKey);
  95. if ImportResult <> ikrSuccess then begin
  96. case ImportResult of
  97. ikrMalformed:
  98. RaiseFatalError('Key file is malformed');
  99. ikrNotPrivateKey:
  100. RaiseFatalError('Key file must be a private key when signing');
  101. end;
  102. RaiseFatalError('Unknown import key result');
  103. end;
  104. end;
  105. procedure CommandExportPublicKey(const AFilename: String);
  106. begin
  107. const Key = TECDSAKey.Create;
  108. try
  109. ImportKey(Key, False);
  110. var PublicKeyText: String;
  111. ISSigExportPublicKeyText(Key, PublicKeyText);
  112. if NewFileExists(AFilename) then begin
  113. const ExistingText = ISSigLoadTextFromFile(AFilename);
  114. if ExistingText = PublicKeyText then begin
  115. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  116. PrintUnlessQuiet('public key unchanged');
  117. Exit;
  118. end else if not Options.AllowOverwrite then
  119. RaiseFatalError('File already exists');
  120. end;
  121. ISSigSaveTextToFile(AFilename, PublicKeyText);
  122. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  123. PrintUnlessQuiet('public key written');
  124. finally
  125. Key.Free;
  126. end;
  127. end;
  128. procedure CommandGeneratePrivateKey;
  129. begin
  130. if not Options.AllowOverwrite and NewFileExists(Options.KeyFile) then
  131. RaiseFatalError('File already exists');
  132. PrintFmtUnlessQuiet('%s: ', [Options.KeyFile], False);
  133. const Key = TECDSAKey.Create;
  134. try
  135. Key.GenerateKeyPair;
  136. var PrivateKeyText: String;
  137. ISSigExportPrivateKeyText(Key, PrivateKeyText);
  138. ISSigSaveTextToFile(Options.KeyFile, PrivateKeyText);
  139. PrintUnlessQuiet('private key written');
  140. finally
  141. Key.Free;
  142. end;
  143. end;
  144. procedure SignSingleFile(const AKey: TECDSAKey; const AFilename: String);
  145. begin
  146. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  147. const FileName = PathExtractName(AFilename);
  148. var FileSize: Int64;
  149. var FileHash: TSHA256Digest;
  150. const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
  151. try
  152. FileSize := F.Size;
  153. FileHash := CalcFileHash(F);
  154. finally
  155. F.Free;
  156. end;
  157. { ECDSA signature output is non-deterministic: signing the same hash with
  158. the same key produces a totally different signature each time. To avoid
  159. unnecessary alterations to the "sig-r" and "sig-s" values when a file is
  160. being re-signed but its contents haven't changed, we attempt to load and
  161. verify the existing .issig file. If the existing values exactly match
  162. what we would have written, then we skip creation of a new .issig file.
  163. Note that "file-name" is compared case-sensitively here because we don't
  164. want to impede the user's ability to correct case mistakes. }
  165. var ExistingFileName: String;
  166. var ExistingFileSize: Int64;
  167. var ExistingFileHash: TSHA256Digest;
  168. const Verified = ISSigVerifySignature(AFilename, [AKey],
  169. ExistingFileName, ExistingFileSize, ExistingFileHash, nil, nil, nil);
  170. if Verified and (FileName = ExistingFileName) and (FileSize = ExistingFileSize) and
  171. SHA256DigestsEqual(FileHash, ExistingFileHash) then begin
  172. PrintUnlessQuiet('signature unchanged');
  173. Exit;
  174. end;
  175. const SigText = ISSigCreateSignatureText(AKey, FileName, FileSize, FileHash);
  176. ISSigSaveTextToFile(AFilename + ISSigExt, SigText);
  177. PrintUnlessQuiet('signature written');
  178. end;
  179. procedure CommandSign(const AFilenames: TStringList);
  180. begin
  181. const Key = TECDSAKey.Create;
  182. try
  183. ImportKey(Key, True);
  184. for var CurFilename in AFilenames do
  185. SignSingleFile(Key, CurFilename);
  186. finally
  187. Key.Free;
  188. end;
  189. end;
  190. function VerifySingleFile(const AKey: TECDSAKey; const AFilename: String): Boolean;
  191. begin
  192. Result := False;
  193. PrintFmtUnlessQuiet('%s: ', [AFilename], False);
  194. var ExpectedFileName: String;
  195. var ExpectedFileSize: Int64;
  196. var ExpectedFileHash: TSHA256Digest;
  197. if not ISSigVerifySignature(AFilename, [AKey], ExpectedFileName, ExpectedFileSize, ExpectedFileHash,
  198. procedure(const Filename: String)
  199. begin
  200. PrintUnlessQuiet('MISSINGFILE (File does not exist)');
  201. end,
  202. procedure(const Filename, SigFilename: String)
  203. begin
  204. PrintUnlessQuiet('MISSINGSIGFILE (Signature file does not exist)');
  205. end,
  206. procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
  207. begin
  208. case VerifyResult of
  209. vsrMalformed, vsrBad:
  210. PrintUnlessQuiet('BADSIGFILE (Signature file is not valid)');
  211. vsrKeyNotFound:
  212. PrintUnlessQuiet('UNKNOWNKEY (Incorrect key ID)');
  213. else
  214. RaiseFatalError('Unknown verify result');
  215. end;
  216. end
  217. ) then
  218. Exit;
  219. if (ExpectedFileName <> '') and not PathSame(PathExtractName(AFilename), ExpectedFileName) then begin
  220. PrintUnlessQuiet('WRONGNAME (File name is incorrect)');
  221. Exit;
  222. end;
  223. const F = TFile.Create(AFilename, fdOpenExisting, faRead, fsRead);
  224. try
  225. if F.Size <> ExpectedFileSize then begin
  226. PrintUnlessQuiet('WRONGSIZE (File size is incorrect)');
  227. Exit;
  228. end;
  229. const ActualFileHash = CalcFileHash(F);
  230. if not SHA256DigestsEqual(ActualFileHash, ExpectedFileHash) then begin
  231. PrintUnlessQuiet('WRONGHASH (File hash is incorrect)');
  232. Exit;
  233. end;
  234. finally
  235. F.Free;
  236. end;
  237. PrintUnlessQuiet('OK');
  238. Result := True;
  239. end;
  240. function CommandVerify(const AFilenames: TStringList): Boolean;
  241. begin
  242. const Key = TECDSAKey.Create;
  243. try
  244. ImportKey(Key, False);
  245. Result := True;
  246. for var CurFilename in AFilenames do
  247. if not VerifySingleFile(Key, CurFilename) then
  248. Result := False;
  249. finally
  250. Key.Free;
  251. end;
  252. end;
  253. procedure ShowUsage;
  254. begin
  255. PrintErrOutput('Inno Setup Signature Tool');
  256. PrintErrOutput('Copyright (C) 1997-2025 Jordan Russell. All rights reserved.');
  257. PrintErrOutput('Portions Copyright (C) 2000-2025 Martijn Laan. All rights reserved.');
  258. PrintErrOutput('https://www.innosetup.com');
  259. PrintErrOutput('');
  260. PrintErrOutput('Usage: issigtool [options] sign <filenames>');
  261. PrintErrOutput('or to verify: issigtool [options] verify <filenames>');
  262. PrintErrOutput('or to export the public key: issigtool [options] export-public-key <filename>');
  263. PrintErrOutput('or to generate a new private key: issigtool [options] generate-private-key');
  264. PrintErrOutput('Options:');
  265. PrintErrOutput(' --key-file=<filename> Specifies the private key filename (overrides ISSIGTOOL_KEY_FILE environment variable)');
  266. PrintErrOutput(' --allow-overwrite, -o Allow to overwrite existing files');
  267. PrintErrOutput(' --quiet, -q Suppresses status messages that are normally printed to standard output');
  268. PrintErrOutput(' --help, -? Prints this information');
  269. PrintErrOutput('');
  270. end;
  271. procedure Go;
  272. begin
  273. const ArgList = TStringList.Create;
  274. try
  275. for var I := 1 to NewParamCount do
  276. ArgList.Add(NewParamStr(I));
  277. const InitialArgListCount = ArgList.Count;
  278. var J := 0;
  279. while J < ArgList.Count do begin
  280. const S = ArgList[J];
  281. if S.StartsWith('-') then begin
  282. if (S = '--help') or (S = '-?') then begin
  283. ShowUsage;
  284. if InitialArgListCount <> 1 then
  285. RaiseFatalErrorFmt('"%s" option cannot be combined with other arguments', [S]);
  286. Exit;
  287. end else if (S = '--allow-overwrite') or (S = '-o') then begin
  288. Options.AllowOverwrite := True;
  289. end else if (S = '--quiet') or (S = '-q') then begin
  290. Options.Quiet := True;
  291. end else if S.StartsWith('--key-file=') then begin
  292. Options.KeyFile := S.Substring(Length('--key-file='));
  293. end else
  294. RaiseFatalErrorFmt('Unknown option "%s".', [S]);
  295. ArgList.Delete(J);
  296. end else begin
  297. if S = '' then
  298. RaiseFatalError('Empty arguments not allowed');
  299. Inc(J);
  300. end;
  301. end;
  302. if ArgList.Count = 0 then begin
  303. ShowUsage;
  304. RaiseFatalError('Missing command argument');
  305. end;
  306. const Command = ArgList[0];
  307. ArgList.Delete(0);
  308. if Options.KeyFile = '' then begin
  309. Options.KeyFile := GetEnv('ISSIGTOOL_KEY_FILE');
  310. if Options.KeyFile = '' then
  311. RaiseFatalError('"--key-file=" option must be specified, ' +
  312. 'or set the ISSIGTOOL_KEY_FILE environment variable');
  313. end;
  314. if Command = 'export-public-key' then begin
  315. if ArgList.Count = 0 then
  316. RaiseFatalError('Missing filename argument')
  317. else if ArgList.Count <> 1 then
  318. RaiseFatalError('Too many arguments');
  319. CommandExportPublicKey(ArgList[0]);
  320. end else if Command = 'generate-private-key' then begin
  321. if ArgList.Count <> 0 then
  322. RaiseFatalError('Too many arguments');
  323. CommandGeneratePrivateKey;
  324. end else if Command = 'sign' then begin
  325. if ArgList.Count = 0 then
  326. RaiseFatalError('Missing filename argument(s)');
  327. CommandSign(ArgList);
  328. end else if Command = 'verify' then begin
  329. if ArgList.Count = 0 then
  330. RaiseFatalError('Missing filename argument(s)');
  331. if not CommandVerify(ArgList) then
  332. Halt(1);
  333. end else
  334. RaiseFatalErrorFmt('Unknown command "%s"', [Command]);
  335. finally
  336. ArgList.Free;
  337. end;
  338. end;
  339. begin
  340. {$IFDEF DEBUG}
  341. ReportMemoryLeaksOnShutdown := True;
  342. {$ENDIF}
  343. StdOutHandle := GetStdHandle(STD_OUTPUT_HANDLE);
  344. StdErrHandle := GetStdHandle(STD_ERROR_HANDLE);
  345. var Mode: DWORD;
  346. StdOutHandleIsConsole := GetConsoleMode(StdOutHandle, Mode);
  347. StdErrHandleIsConsole := GetConsoleMode(StdErrHandle, Mode);
  348. try
  349. Go;
  350. except
  351. PrintErrOutput('issigtool fatal error: ' + GetExceptMessage);
  352. Halt(2);
  353. end;
  354. end.