ISSigFunc.pas 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. unit ISSigFunc;
  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. Functions for creating/verifying .issig signatures and importing/exporting
  8. text-based keys
  9. }
  10. interface
  11. uses
  12. Windows, SysUtils, Classes, ECDSA, SHA256;
  13. type
  14. TISSigVerifySignatureResult = (vsrSuccess, vsrMalformed, vsrKeyNotFound,
  15. vsrBadSignature);
  16. TISSigImportKeyResult = (ikrSuccess, ikrMalformed, ikrNotPrivateKey);
  17. { Preferred, hardened functions for loading/saving .issig and key file text }
  18. function ISSigLoadTextFromFile(const AFilename: String): String;
  19. procedure ISSigSaveTextToFile(const AFilename, AText: String);
  20. function ISSigCreateSignatureText(const AKey: TECDSAKey;
  21. const AFileSize: Int64; const AFileHash: TSHA256Digest): String;
  22. function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
  23. const AText: String; out AFileSize: Int64;
  24. out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
  25. procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
  26. var APrivateKeyText: String);
  27. procedure ISSigExportPublicKeyText(const AKey: TECDSAKey;
  28. var APublicKeyText: String);
  29. function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
  30. const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
  31. function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
  32. implementation
  33. uses
  34. StringScanner;
  35. const
  36. ISSigTextFileLengthLimit = 500;
  37. NonControlASCIICharsSet = [#32..#126];
  38. DigitsSet = ['0'..'9'];
  39. HexDigitsSet = DigitsSet + ['a'..'f'];
  40. function ECDSAInt256ToString(const Value: TECDSAInt256): String;
  41. begin
  42. Result := SHA256DigestToString(TSHA256Digest(Value));
  43. end;
  44. function ECDSAInt256FromString(const S: String): TECDSAInt256;
  45. begin
  46. TSHA256Digest(Result) := SHA256DigestFromString(S);
  47. end;
  48. function CalcHashToSign(const AFileSize: Int64;
  49. const AFileHash: TSHA256Digest): TSHA256Digest;
  50. begin
  51. var Context: TSHA256Context;
  52. SHA256Init(Context);
  53. SHA256Update(Context, AFileSize, SizeOf(AFileSize));
  54. SHA256Update(Context, AFileHash, SizeOf(AFileHash));
  55. Result := SHA256Final(Context);
  56. end;
  57. function CalcKeyID(const APublicKey: TECDSAPublicKey): TSHA256Digest;
  58. begin
  59. Result := SHA256Buf(APublicKey, SizeOf(APublicKey));
  60. end;
  61. function ConsumeLineValue(var SS: TStringScanner; const AIdent: String;
  62. var AValue: String; const AMinValueLength, AMaxValueLength: Integer;
  63. const AAllowedChars: TSysCharSet): Boolean;
  64. begin
  65. Result := False;
  66. if SS.Consume(AIdent) and SS.Consume(' ') then
  67. if SS.ConsumeMultiToString(AAllowedChars, AValue, AMinValueLength,
  68. AMaxValueLength) > 0 then begin
  69. { CRLF and LF line breaks are allowed (but not CR) }
  70. SS.Consume(#13);
  71. Result := SS.Consume(#10);
  72. end;
  73. end;
  74. function ISSigLoadTextFromFile(const AFilename: String): String;
  75. { Reads the specified file's contents into a string. This is intended only for
  76. loading .issig and key files. If the file appears to be invalid (e.g., if
  77. it is too large or contains invalid characters), then an empty string is
  78. returned, which will be reported as malformed when it is processed by
  79. ISSigVerifySignatureText or ISSigImportKeyText. }
  80. begin
  81. var U: UTF8String;
  82. SetLength(U, ISSigTextFileLengthLimit + 1);
  83. const F = TFileStream.Create(AFilename, fmOpenRead or fmShareDenyWrite);
  84. try
  85. const BytesRead = F.Read(U[Low(U)], Length(U));
  86. if BytesRead >= Length(U) then
  87. Exit('');
  88. SetLength(U, BytesRead);
  89. finally
  90. F.Free;
  91. end;
  92. { Defense-in-depth: Reject any non-CRLF control characters up front, as well
  93. as any non-ASCII characters (to avoid any possible issues with converting
  94. invalid multibyte characters) }
  95. for var C in U do
  96. if not CharInSet(C, [#10, #13, #32..#126]) then
  97. Exit('');
  98. Result := String(U);
  99. end;
  100. procedure ISSigSaveTextToFile(const AFilename, AText: String);
  101. begin
  102. const F = TFileStream.Create(AFilename, fmCreate or fmShareExclusive);
  103. try
  104. const U = UTF8String(AText);
  105. if U <> '' then
  106. F.WriteBuffer(U[Low(U)], Length(U));
  107. finally
  108. F.Free;
  109. end;
  110. end;
  111. function ISSigCreateSignatureText(const AKey: TECDSAKey;
  112. const AFileSize: Int64; const AFileHash: TSHA256Digest): String;
  113. begin
  114. { File size is limited to 16 digits (enough for >9 EB) }
  115. if (AFileSize < 0) or (AFileSize > 9_999_999_999_999_999) then
  116. raise Exception.Create('File size out of range');
  117. var PublicKey: TECDSAPublicKey;
  118. AKey.ExportPublicKey(PublicKey);
  119. const HashToSign = CalcHashToSign(AFileSize, AFileHash);
  120. var Sig: TECDSASignature;
  121. AKey.SignHash(HashToSign, Sig);
  122. Result := Format(
  123. 'format issig-v1'#13#10 +
  124. 'file-size %d'#13#10 +
  125. 'file-hash %s'#13#10 +
  126. 'key-id %s'#13#10 +
  127. 'sig-r %s'#13#10 +
  128. 'sig-s %s'#13#10,
  129. [AFileSize,
  130. SHA256DigestToString(AFileHash),
  131. SHA256DigestToString(CalcKeyID(PublicKey)),
  132. ECDSAInt256ToString(Sig.Sig_r),
  133. ECDSAInt256ToString(Sig.Sig_s)]);
  134. end;
  135. function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
  136. const AText: String; out AFileSize: Int64;
  137. out AFileHash: TSHA256Digest): TISSigVerifySignatureResult;
  138. var
  139. TextValues: record
  140. Format, FileSize, FileHash, KeyID, Sig_r, Sig_s: String;
  141. end;
  142. begin
  143. { To be extra safe, clear the "out" parameters just in case the caller isn't
  144. properly checking the function result }
  145. AFileSize := -1;
  146. FillChar(AFileHash, SizeOf(AFileHash), 0);
  147. if Length(AText) > ISSigTextFileLengthLimit then
  148. Exit(vsrMalformed);
  149. var SS := TStringScanner.Create(AText);
  150. if not ConsumeLineValue(SS, 'format', TextValues.Format, 8, 8, NonControlASCIICharsSet) or
  151. (TextValues.Format <> 'issig-v1') or
  152. not ConsumeLineValue(SS, 'file-size', TextValues.FileSize, 1, 16, DigitsSet) or
  153. not ConsumeLineValue(SS, 'file-hash', TextValues.FileHash, 64, 64, HexDigitsSet) or
  154. not ConsumeLineValue(SS, 'key-id', TextValues.KeyID, 64, 64, HexDigitsSet) or
  155. not ConsumeLineValue(SS, 'sig-r', TextValues.Sig_r, 64, 64, HexDigitsSet) or
  156. not ConsumeLineValue(SS, 'sig-s', TextValues.Sig_s, 64, 64, HexDigitsSet) or
  157. not SS.ReachedEnd then
  158. Exit(vsrMalformed);
  159. { Don't allow leading zeros on file-size }
  160. if (Length(TextValues.FileSize) > 1) and
  161. (TextValues.FileSize[Low(TextValues.FileSize)] = '0') then
  162. Exit(vsrMalformed);
  163. { Find the key that matches the key ID }
  164. var KeyUsed: TECDSAKey := nil;
  165. const KeyID = SHA256DigestFromString(TextValues.KeyID);
  166. for var K in AAllowedKeys do begin
  167. var PublicKey: TECDSAPublicKey;
  168. K.ExportPublicKey(PublicKey);
  169. if SHA256DigestsEqual(KeyID, CalcKeyID(PublicKey)) then begin
  170. KeyUsed := K;
  171. Break;
  172. end;
  173. end;
  174. if KeyUsed = nil then
  175. Exit(vsrKeyNotFound);
  176. const UnverifiedFileSize = StrToInt64(TextValues.FileSize);
  177. const UnverifiedFileHash = SHA256DigestFromString(TextValues.FileHash);
  178. const HashToSign = CalcHashToSign(UnverifiedFileSize, UnverifiedFileHash);
  179. var Sig: TECDSASignature;
  180. Sig.Sig_r := ECDSAInt256FromString(TextValues.Sig_r);
  181. Sig.Sig_s := ECDSAInt256FromString(TextValues.Sig_s);
  182. if KeyUsed.VerifySignature(HashToSign, Sig) then begin
  183. AFileSize := UnverifiedFileSize;
  184. AFileHash := UnverifiedFileHash;
  185. Result := vsrSuccess;
  186. end else
  187. Result := vsrBadSignature;
  188. end;
  189. procedure ISSigExportPrivateKeyText(const AKey: TECDSAKey;
  190. var APrivateKeyText: String);
  191. begin
  192. var PrivateKey: TECDSAPrivateKey;
  193. try
  194. AKey.ExportPrivateKey(PrivateKey);
  195. APrivateKeyText := Format(
  196. 'format issig-private-key'#13#10 +
  197. 'key-id %s'#13#10 +
  198. 'public-x %s'#13#10 +
  199. 'public-y %s'#13#10 +
  200. 'private-d %s'#13#10,
  201. [SHA256DigestToString(CalcKeyID(PrivateKey.PublicKey)),
  202. ECDSAInt256ToString(PrivateKey.PublicKey.Public_x),
  203. ECDSAInt256ToString(PrivateKey.PublicKey.Public_y),
  204. ECDSAInt256ToString(PrivateKey.Private_d)]);
  205. finally
  206. PrivateKey.Clear;
  207. end;
  208. end;
  209. procedure ISSigExportPublicKeyText(const AKey: TECDSAKey;
  210. var APublicKeyText: String);
  211. begin
  212. var PublicKey: TECDSAPublicKey;
  213. try
  214. AKey.ExportPublicKey(PublicKey);
  215. APublicKeyText := Format(
  216. 'format issig-public-key'#13#10 +
  217. 'key-id %s'#13#10 +
  218. 'public-x %s'#13#10 +
  219. 'public-y %s'#13#10,
  220. [SHA256DigestToString(CalcKeyID(PublicKey)),
  221. ECDSAInt256ToString(PublicKey.Public_x),
  222. ECDSAInt256ToString(PublicKey.Public_y)]);
  223. finally
  224. PublicKey.Clear;
  225. end;
  226. end;
  227. function ISSigImportKeyText(const AKey: TECDSAKey; const AText: String;
  228. const ANeedPrivateKey: Boolean): TISSigImportKeyResult;
  229. var
  230. TextValues: record
  231. Format, KeyID, Public_x, Public_y, Private_d: String;
  232. end;
  233. begin
  234. Result := ikrMalformed;
  235. if Length(AText) > ISSigTextFileLengthLimit then
  236. Exit;
  237. var SS := TStringScanner.Create(AText);
  238. if not ConsumeLineValue(SS, 'format', TextValues.Format, 16, 17, NonControlASCIICharsSet) then
  239. Exit;
  240. var HasPrivateKey := False;
  241. if TextValues.Format = 'issig-private-key' then
  242. HasPrivateKey := True
  243. else if TextValues.Format = 'issig-public-key' then
  244. { already False }
  245. else
  246. Exit;
  247. if not ConsumeLineValue(SS, 'key-id', TextValues.KeyID, 64, 64, HexDigitsSet) or
  248. not ConsumeLineValue(SS, 'public-x', TextValues.Public_x, 64, 64, HexDigitsSet) or
  249. not ConsumeLineValue(SS, 'public-y', TextValues.Public_y, 64, 64, HexDigitsSet) then
  250. Exit;
  251. if HasPrivateKey then
  252. if not ConsumeLineValue(SS, 'private-d', TextValues.Private_d, 64, 64, HexDigitsSet) then
  253. Exit;
  254. if not SS.ReachedEnd then
  255. Exit;
  256. var PrivateKey: TECDSAPrivateKey;
  257. PrivateKey.PublicKey.Public_x := ECDSAInt256FromString(TextValues.Public_x);
  258. PrivateKey.PublicKey.Public_y := ECDSAInt256FromString(TextValues.Public_y);
  259. { Verify that the key ID is correct for the public key values }
  260. if not SHA256DigestsEqual(SHA256DigestFromString(TextValues.KeyID),
  261. CalcKeyID(PrivateKey.PublicKey)) then
  262. Exit;
  263. if ANeedPrivateKey then begin
  264. if not HasPrivateKey then
  265. Exit(ikrNotPrivateKey);
  266. PrivateKey.Private_d := ECDSAInt256FromString(TextValues.Private_d);
  267. AKey.ImportPrivateKey(PrivateKey);
  268. end else
  269. AKey.ImportPublicKey(PrivateKey.PublicKey);
  270. Result := ikrSuccess;
  271. end;
  272. function ISSigCalcStreamHash(const AStream: TStream): TSHA256Digest;
  273. var
  274. Buf: array[0..$FFFF] of Byte;
  275. begin
  276. var Context: TSHA256Context;
  277. SHA256Init(Context);
  278. while True do begin
  279. const BytesRead = Cardinal(AStream.Read(Buf, SizeOf(Buf)));
  280. if BytesRead = 0 then
  281. Break;
  282. SHA256Update(Context, Buf, BytesRead);
  283. end;
  284. Result := SHA256Final(Context);
  285. end;
  286. end.