ISSigTool.dpr 12 KB

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