/
SignTool.fs
546 lines (494 loc) · 25.6 KB
/
SignTool.fs
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
namespace Fake.Tools
/// <summary>
/// The <a href="https://docs.microsoft.com/en-gb/windows/win32/seccrypto/signtool">SignTool</a> tool is a
/// command-line tool that digitally signs files, verifies signatures in files, or time stamps files.
/// </summary>
/// <remarks>
/// <a href="/articles/tools-signtool.html">Documentation & samples</a>
/// </remarks>
///
///
[<RequireQualifiedAccess>]
module SignTool =
open System
open System.IO
open System.Text
open Fake.Core
open Fake.IO
open Fake.IO.Globbing.Operators
/// Verbosity
type Verbosity =
/// Displays no output on successful execution and minimal output for failed execution. (signtool option: /q)
| Quiet
/// Displays verbose output for successful execution, failed execution, and warning messages. (signtool option: /v)
| Verbose
/// Digest algorithm
type DigestAlgorithm =
| SHA1
| SHA256
/// <summary>
/// Specifies the URL of the time stamp server and the digest algorithm used by the RFC 3161 time stamp server.
/// </summary>
type TimeStampOption =
{
/// Specifies the URL of the time stamp server. (signtool options: /t URL, /tr URL)
ServerUrl: string
/// Used to request a digest algorithm used by the RFC 3161 time stamp server. (signtool option: /td alg)
Algorithm: DigestAlgorithm option
}
/// Options default values.
static member Create(serverUrl) =
{ ServerUrl = serverUrl
Algorithm = None }
/// <summary>
/// Specifies parameters to use when using a certificate from a file.
/// </summary>
type CertificateFromFile =
{
/// Specifies the signing certificate in a file. Only the Personal Information Exchange (PFX) file format is
/// supported. If the file is in PFX format protected by a password, use the /p option to specify the password. If the file does not contain private keys, use the /csp and /k options to specify the CSP and private key container name, respectively. (signtool option: /f SignCertFile)
Path: string
/// Specifies the password to use when opening a PFX file. A PFX file can be specified by using the /f option.
/// (signtool option: /p Password)
Password: string option
/// Specifies the cryptographic service provider (CSP) that contains the private key container. (signtool
/// option: /csp CSPName)
CspName: string option
/// Specifies the key that contains the name of the private key. (signtool option: /kc Name)
PrivateKeyKey: string option
}
/// Options default values.
static member Create(path) =
{ Path = path
Password = None
CspName = None
PrivateKeyKey = None }
/// <summary>
/// Specifies parameters to use when using a certificate from a certificate store.
/// </summary>
type CertificateFromStore =
{
/// Selects the best signing certificate automatically. If this option is not present, SignTool expects
/// to find only one valid signing certificate. (signtool option: /a)
AutomaticallySelectCertificate: bool option
/// Specifies the name of the issuer of the signing certificate. This value can be a substring of the
/// entire issuer name. (signtool option: /i IssuerName)
IssuerName: string option
/// Specifies the name of the subject of the signing certificate. This value can be a substring of the
/// entire subject name. (signtool option: /n SubjectName)
SubjectName: string option
/// Specifies the name of the subject of the root certificate that the signing certificate must chain to.
/// This value can be a substring of the entire subject name of the root certificate. (signtool option:
/// /r RootSubjectName)
RootSubjectName: string option
/// Specifies the store to open when searching for the certificate. If this option is not specified,
/// the My store is opened. If the store does not exist, signtool will wail with a "File not found" error.
/// (signtool option: /s StoreName)
StoreName: string option
/// Specifies the SHA1 hash of the signing certificate. When viewing a certificate, this is the value of
/// the Thumbprint field. (signtool option: /sha1 Hash)
Hash: string option
/// Specifies that a computer store, instead of a user store, be used. Accessing the computer store
/// requires admin rights. If the process does not have admin rights, no certificates will be found.
/// (signtool option: /sm)
UseComputerStore: bool option
}
/// Options default values.
static member Create() =
{ AutomaticallySelectCertificate = None
IssuerName = None
SubjectName = None
RootSubjectName = None
StoreName = None
Hash = None
UseComputerStore = None }
/// Specifies what type of certificate to use.
type SignCertificate =
/// Use a certificate stored in a file.
| File of CertificateFromFile
/// Use a certificate stored in a certificate store.
| Store of CertificateFromStore
/// Use a certificate stored in a file with options
static member FromFile(path, setOptions) =
let options = setOptions (CertificateFromFile.Create(path))
File options
/// Use a certificate stored in a certificate store with options.
static member FromStore(setOptions) =
let options = setOptions (CertificateFromStore.Create())
Store options
/// <summary>
/// Sign command options
/// </summary>
type SignOptions =
{
/// Specifies the certificate to use for signing. (signtool options: /a, /f, /p, /csp, /kc, /i, /n, /r,
/// /s, /sha1, /sm)
Certificate: SignCertificate
/// Specifies the file digest algorithm to use to create file signatures. The default algorithm is Secure
/// Hash Algorithm (SHA-1). (signtool option: /fd)
DigestAlgorithm: DigestAlgorithm option
/// Specifies a file that contains an additional certificate to add to the signature block.
/// (signtool option: /ac FileName)
AdditionalCertificate: string option
/// Appends this signature. If no primary signature is present, this signature is made the primary signature.
/// (signtool option: /as)
AppendSignature: bool option
/// Specifies the Certificate Template Name (a Microsoft extension) for the signing certificate.
/// (signtool option: /c CertTemplateName)
CertificateTemplateName: string option
/// Specifies a description of the signed content. (signtool option: /d Desc)
Description: string option
/// Specifies the enhanced key usage (EKU) that must be present in the signing certificate.
/// The usage value can be specified by OID or string. The default usage is "Code Signing"
/// (1.3.6.1.5.5.7.3.3). (signtool option: /u Usage)
EnhancedKeyUsage: string option
/// Specifies using "Windows System Component Verification" (1.3.6.1.4.1.311.10.3.6). (signtool option: /uw)
WindowsSystemComponentVerification: bool option
/// Displays debugging information. (signtool option: /debug)
Debug: bool option
/// Output verbosity. (signtool options: /q, /v)
Verbosity: Verbosity option
/// Path to signtool.exe.
/// If not provided, an attempt will be made to locate it automatically in 'Program Files (x86)\Windows Kits'.
ToolPath: string option
/// Timeout.
Timeout: TimeSpan option
/// Working directory.
/// If not provided, current directory will be used.
WorkingDir: string option
}
/// Options default values.
static member Create(certificate) =
{ Certificate = certificate
DigestAlgorithm = None
AdditionalCertificate = None
AppendSignature = None
CertificateTemplateName = None
Description = None
EnhancedKeyUsage = None
WindowsSystemComponentVerification = None
Debug = None
Verbosity = None
ToolPath = None
Timeout = None
WorkingDir = None }
/// <summary>
/// Timestamp command options
/// </summary>
type TimeStampOptions =
{
/// Specifies the URL of the time stamp server. (signtool options: /t URL, /tr URL)
ServerUrl: string
/// Used to request a digest algorithm used by the RFC 3161 time stamp server. (signtool option: /td alg)
Algorithm: DigestAlgorithm option
/// Adds a timestamp to the signature at index. (signtool option: /tp Index)
TimestampIndex: int option
/// Displays debugging information. (signtool option: /debug)
Debug: bool option
/// Output verbosity. (signtool options: /q, /v)
Verbosity: Verbosity option
/// Path to signtool.exe.
/// If not provided, an attempt will be made to locate it automatically in 'Program Files (x86)\Windows Kits'.
ToolPath: string option
/// Timeout.
Timeout: TimeSpan option
/// Working directory.
/// If not provided, current directory will be used.
WorkingDir: string option
}
/// Options default values.
static member Create(serverUrl) =
{ ServerUrl = serverUrl
Algorithm = None
TimestampIndex = None
Debug = None
Verbosity = None
ToolPath = None
Timeout = None
WorkingDir = None }
/// <summary>
/// Verify command options
/// </summary>
type VerifyOptions =
{
/// Specifies that all methods can be used to verify the file. First, the catalog databases are
/// searched to determine whether the file is signed in a catalog. If the file is not signed in any
/// catalog, SignTool attempts to verify the file's embedded signature. This option is recommended
/// when verifying files that may or may not be signed in a catalog. Examples of files that may or
/// may not be signed include Windows files or drivers. (signtool option: /a)
AllMethods: bool option
/// Verifies all signatures in a file with multiple signatures. (signtool option: /all)
AllSignatures: bool option
/// Print the description and description URL. (signtool option: /d)
PrintDescription: bool option
/// Verifies the signature at a certain position. (signtool option: /ds Index)
VerifyIndex: int option
/// Performs the verification by using the x64 kernel-mode driver signing policy. (signtool option: /kp)
UseX64KernelModeDriverSigningPolicy: bool option
/// Uses multiple verification semantics. This is the default behavior of a WinVerifyTrust call.
/// (signtool option: /ms)
UseMultipleVerificationSemantics: bool option
/// Verifies the file by operating system version. The version parameter is of the form:
/// PlatformID**:VerMajor.VerMinor.**BuildNumber. The use of the /o switch is recommended.
/// If /o is not specified SignTool may return unexpected results. For example, if you do not include
/// the /o switch, then system catalogs that validate correctly on an older OS may not validate
/// correctly on a newer OS. (signtool option: /o Version)
VerifyByOperatingSystemVersion: string option
/// Specifies that the Default Authentication Verification Policy is used. If the /pa option is not
/// specified, SignTool uses the Windows Driver Verification Policy. This option cannot be used with
/// the catdb options. (signtool option: /pa)
UseDefaultAuthenticationVerificationPolicy: bool option
/// Specifies the name of the subject of the root certificate that the signing certificate must chain to.
/// This value can be a substring of the entire subject name of the root certificate.
/// (signtool option: /r RootSubjectName)
RootSubjectName: string option
/// Specifies that a warning is generated if the signature is not time stamped. (signtool option: /tw)
WarnIfNotTimeStamped: bool option
/// Displays debugging information. (signtool option: /debug)
Debug: bool option
/// Output verbosity. (signtool options: /q, /v)
Verbosity: Verbosity option
/// Path to signtool.exe.
/// If not provided, an attempt will be made to locate it automatically in 'Program Files (x86)\Windows Kits'.
ToolPath: string option
/// Timeout.
Timeout: TimeSpan option
/// Working directory.
/// If not provided, current directory will be used.
WorkingDir: string option
}
/// Options default values.
static member Create() =
{ AllMethods = None
AllSignatures = None
PrintDescription = None
VerifyIndex = None
UseX64KernelModeDriverSigningPolicy = None
UseMultipleVerificationSemantics = None
VerifyByOperatingSystemVersion = None
UseDefaultAuthenticationVerificationPolicy = None
RootSubjectName = None
WarnIfNotTimeStamped = None
Debug = None
Verbosity = None
ToolPath = None
Timeout = None
WorkingDir = None }
/// run signTool command with options and files
let internal signTool
runner
(signToolExeLocator: unit -> string option)
(args: Arguments)
toolPath
timeout
workingDir
=
let signToolPath =
match toolPath with
| Some p -> p
| None ->
match signToolExeLocator () with
| Some p -> p
| None ->
failwith
"SignTool failed: Could not locate signtool.exe. Make sure you have Windows SDKs installed or provide direct path in the ToolPath option."
let signToolWorkingDir =
workingDir |> Option.defaultValue (Directory.GetCurrentDirectory())
runner signToolPath args signToolWorkingDir timeout
/// default runner
let internal defaultRunner
(signToolPath: string)
(signToolArgs: Arguments)
(signToolWorkingDir: string)
(signToolTimeout: TimeSpan option)
=
let stdOut = StringBuilder()
let stdErr = StringBuilder()
let result =
CreateProcess.fromCommand (RawCommand(signToolPath, signToolArgs))
|> CreateProcess.withWorkingDirectory signToolWorkingDir
|> (fun cp ->
match signToolTimeout with
| Some t -> CreateProcess.withTimeout t cp
| None -> cp)
|> CreateProcess.redirectOutput
|> CreateProcess.withOutputEvents (stdOut.AppendLine >> ignore) (stdErr.AppendLine >> ignore)
|> Proc.run
Trace.log (stdOut.ToString())
if result.ExitCode <> 0 then
sprintf "SignTool failed: %s" (stdErr.ToString())
|> TraceSecrets.guardMessage
|> failwith
/// default signtool.exe locator
let internal defaultSignToolExeLocator () =
let winSdksDirs =
seq {
// tryFindFile doesn't understand globbing, so it has to be done beforehand
yield!
!!(Environment.ProgramFilesX86 + @"\Windows Kits\10\bin\**\x86")
|> Seq.sortDescending
yield @"[ProgramFilesX86]\Windows Kits\8.1\bin\x86"
}
ProcessUtils.tryFindFile winSdksDirs "signtool.exe"
/// append common arguments
let private commonArguments command debug verbosity arguments =
arguments
|> Arguments.withPrefix [ command ]
|> Arguments.appendIf (debug |> Option.defaultValue false) "/debug"
|> fun args ->
match verbosity with
| Some v ->
match v with
| Quiet -> args |> Arguments.append [ "/q" ]
| Verbose -> args |> Arguments.append [ "/v" ]
| None -> args
/// append "sign"-specific arguments
let private signArguments (options: SignOptions) additionalArguments files =
let signToolArgs =
Arguments.Empty
|> commonArguments "sign" options.Debug options.Verbosity
|> Arguments.appendIf (options.AppendSignature |> Option.defaultValue false) "/as"
|> fun args ->
match options.Certificate with
| File f ->
args
|> Arguments.append [ "/f"; f.Path ]
|> Arguments.appendOption "/p" f.Password
|> Arguments.appendOption "/csp" f.CspName
|> Arguments.appendOption "/kc" f.PrivateKeyKey
| Store s ->
args
|> Arguments.appendIf (s.AutomaticallySelectCertificate |> Option.defaultValue false) "/a"
|> Arguments.appendOption "/i" s.IssuerName
|> Arguments.appendOption "/n" s.SubjectName
|> Arguments.appendOption "/r" s.RootSubjectName
|> Arguments.appendOption "/s" s.StoreName
|> Arguments.appendOption "/sha1" s.Hash
|> Arguments.appendIf (s.UseComputerStore |> Option.defaultValue false) "/sm"
|> fun args ->
match options.DigestAlgorithm with
| None -> args
| Some SHA1 -> args |> Arguments.append [ "/fd"; "sha1" ]
| Some SHA256 -> args |> Arguments.append [ "/fd"; "sha256" ]
|> Arguments.appendOption "/ac" options.AdditionalCertificate
|> Arguments.appendOption "/c" options.CertificateTemplateName
|> Arguments.appendOption "/d" options.Description
|> Arguments.appendOption "/u" options.EnhancedKeyUsage
|> Arguments.appendIf (options.WindowsSystemComponentVerification |> Option.defaultValue false) "/uw"
|> additionalArguments
|> Arguments.append files
signToolArgs
/// append "timestamp"-specific arguments
let private timestampArguments serverUrl algorithm arguments =
match algorithm with
| None
| Some SHA1 -> arguments |> Arguments.append [ "/t"; serverUrl ]
| Some SHA256 ->
// Note from signtool.exe docs:
// The /td switch must be declared after the /tr switch, not before.
// If the /td switch is declared before the /tr switch, the timestamp that is returned is from an SHA1 algorithm instead of the intended SHA256 algorithm.
arguments |> Arguments.append [ "/tr"; serverUrl; "/td"; "sha256" ]
/// hide password in trace output
let private hidePasswordInTrace certificate =
match Context.isFakeContext (), certificate with
| true, File f ->
if f.Password.IsSome then
TraceSecrets.register "<PASSWORD>" f.Password.Value
| _ -> ()
/// run the sign command using a runner
let internal signInternal runner signToolExeLocator (options: SignOptions) (files: seq<string>) =
let signToolArgs = signArguments options (fun args -> args) files
hidePasswordInTrace options.Certificate |> ignore
signTool runner signToolExeLocator signToolArgs options.ToolPath options.Timeout options.WorkingDir
/// run the sign command with time stamping using a runner
let internal signWithTimeStampInternal
runner
signToolExeLocator
(signOptions: SignOptions)
(timeStampOptions: TimeStampOption)
(files: seq<string>)
=
let signToolArgs =
signArguments signOptions (timestampArguments timeStampOptions.ServerUrl timeStampOptions.Algorithm) files
hidePasswordInTrace signOptions.Certificate
signTool runner signToolExeLocator signToolArgs signOptions.ToolPath signOptions.Timeout signOptions.WorkingDir
/// run the timestamp command using a runner
let internal timeStampInternal runner signToolExeLocator (options: TimeStampOptions) (files: seq<string>) =
let signToolArgs =
Arguments.Empty
|> commonArguments "timestamp" options.Debug options.Verbosity
|> timestampArguments options.ServerUrl options.Algorithm
|> Arguments.appendOption "/tp" (options.TimestampIndex |> Option.map (fun i -> i.ToString()))
|> Arguments.append files
signTool runner signToolExeLocator signToolArgs options.ToolPath options.Timeout options.WorkingDir
/// run the verify command using a runner
let internal verifyInternal runner signToolExeLocator (options: VerifyOptions) (files: seq<string>) =
let signToolArgs =
Arguments.Empty
|> commonArguments "verify" options.Debug options.Verbosity
|> Arguments.appendIf (options.AllMethods |> Option.defaultValue false) "/a"
|> Arguments.appendIf (options.AllSignatures |> Option.defaultValue false) "/all"
|> Arguments.appendIf (options.PrintDescription |> Option.defaultValue false) "/d"
|> Arguments.appendOption "/ds" (options.VerifyIndex |> Option.map (fun i -> i.ToString()))
|> Arguments.appendIf (options.UseX64KernelModeDriverSigningPolicy |> Option.defaultValue false) "/kp"
|> Arguments.appendIf (options.UseMultipleVerificationSemantics |> Option.defaultValue false) "/ms"
|> Arguments.appendOption "/o" options.VerifyByOperatingSystemVersion
|> Arguments.appendIf
(options.UseDefaultAuthenticationVerificationPolicy |> Option.defaultValue false)
"/pa"
|> Arguments.appendOption "/r" options.RootSubjectName
|> Arguments.appendIf (options.WarnIfNotTimeStamped |> Option.defaultValue false) "/tw"
|> Arguments.append files
signTool runner signToolExeLocator signToolArgs options.ToolPath options.Timeout options.WorkingDir
/// <summary>
/// Signs files according to the options specified.
/// </summary>
///
/// <param name="certificate">The signing certificate</param>
/// <param name="setOptions">The sign tool options</param>
/// <param name="files">The files list to sign</param>
let sign (certificate: SignCertificate) (setOptions: SignOptions -> SignOptions) (files: seq<string>) =
let options = setOptions (SignOptions.Create(certificate))
signInternal defaultRunner defaultSignToolExeLocator options files
/// <summary>
/// Signs and time stamps files according to the options specified.
/// </summary>
///
/// <param name="certificate">The signing certificate</param>
/// <param name="setSignOptions">The sign tool options</param>
/// <param name="serverUrl">The timestamp server URL</param>
/// <param name="setTimeStampOptions">The signing timestamp options</param>
/// <param name="files">The files list to sign</param>
let signWithTimeStamp
(certificate: SignCertificate)
(setSignOptions: SignOptions -> SignOptions)
(serverUrl: string)
(setTimeStampOptions: TimeStampOption -> TimeStampOption)
(files: seq<string>)
=
let signOptions = setSignOptions (SignOptions.Create(certificate))
let timeStampOptions = setTimeStampOptions (TimeStampOption.Create(serverUrl))
signWithTimeStampInternal defaultRunner defaultSignToolExeLocator signOptions timeStampOptions files
/// <summary>
/// Time stamps files according to the options specified. The files being time stamped must
/// have previously been signed.
/// </summary>
///
/// <param name="serverUrl">The timestamp server URL</param>
/// <param name="setOptions">The signing timestamp options</param>
/// <param name="files">The files list to sign</param>
let timeStamp (serverUrl: string) (setOptions: TimeStampOptions -> TimeStampOptions) (files: seq<string>) =
let options = setOptions (TimeStampOptions.Create(serverUrl))
timeStampInternal defaultRunner defaultSignToolExeLocator options files
/// <summary>
/// Verifies files according to the options specified.
/// The SignTool verify command determines whether the signing certificate was issued by a trusted authority,
/// whether the signing certificate has been revoked, and, optionally, whether the signing certificate is
/// valid for a specific policy.
/// </summary>
///
/// <param name="setOptions">The verifying options</param>
/// <param name="files">The files list to verify the signing</param>
let verify (setOptions: VerifyOptions -> VerifyOptions) (files: seq<string>) =
let options = setOptions (VerifyOptions.Create())
verifyInternal defaultRunner defaultSignToolExeLocator options files