-
-
Notifications
You must be signed in to change notification settings - Fork 973
/
Copy pathISSigFunc.pas
331 lines (286 loc) · 10.7 KB
/
ISSigFunc.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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.