diff --git a/README.md b/README.md index 938187a..8fad58c 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,9 @@ The `--help` or `sign --help` option provides more detail about each parameter. the system will use the default based on the number of available processor threads. Setting this value to "1" disable concurrent signing. +* `--signing-throttle` [short: `-st`, required: no]: When specified instructs the tool to throttle the rate of + signing operations. Cannot be combined with -mdop (other than with a value of 1). The value is the minimum time between requests in seconds. + In most circumances, using the defaults for page hashing is recommended, which can be done by simply omitting both of the parameters. ## Supported Formats diff --git a/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs b/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs index d36df3f..4c73824 100644 --- a/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs +++ b/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Threading; namespace AzureSign.Core { @@ -19,20 +20,19 @@ public class AuthenticodeKeyVaultSigner : IDisposable private readonly TimeStampConfiguration _timeStampConfiguration; private readonly MemoryCertificateStore _certificateStore; private readonly X509Chain _chain; - private readonly SignCallback _signCallback; - + private readonly SignCallback _signCallback; /// /// Creates a new instance of . /// /// - /// An instance of an asymmetric algorithm that will be used to sign. It must support signing with - /// a private key. + /// An instance of an asymmetric algorithm that will be used to sign. It must support signing with + /// a private key. /// /// The X509 public certificate for the . /// The digest algorithm to sign the file. /// The timestamp configuration for timestamping the file. To omit timestamping, - /// use . + /// use . /// Any additional certificates to assist in building a certificate chain. public AuthenticodeKeyVaultSigner(AsymmetricAlgorithm signingAlgorithm, X509Certificate2 signingCertificate, HashAlgorithmName fileDigestAlgorithm, TimeStampConfiguration timeStampConfiguration, @@ -66,16 +66,17 @@ public AuthenticodeKeyVaultSigner(AsymmetricAlgorithm signingAlgorithm, X509Cert } /// Authenticode signs a file. - /// True if the signing process should try to include page hashing, otherwise false. - /// Use null to use the operating system default. Note that page hashing still may be disabled if the - /// Subject Interface Package does not support page hashing. - /// A URL describing the signature or the signer. - /// The description to apply to the signature. /// The path to the file to signed. + /// The description to apply to the signature. + /// A URL describing the signature or the signer. + /// True if the signing process should try to include page hashing, otherwise false. + /// Use null to use the operating system default. Note that page hashing still may be disabled if the + /// Subject Interface Package does not support page hashing. /// An optional logger to capture signing operations. /// A HRESULT indicating the result of the signing operation. S_OK, or zero, is returned if the signing /// operation completed successfully. - public unsafe int SignFile(ReadOnlySpan path, ReadOnlySpan description, ReadOnlySpan descriptionUrl, bool? pageHashing, ILogger? logger = null) + public unsafe int SignFile(ReadOnlySpan path, ReadOnlySpan description, + ReadOnlySpan descriptionUrl, bool? pageHashing, ILogger? logger = null) { static char[] NullTerminate(ReadOnlySpan str) { @@ -197,6 +198,7 @@ static char[] NullTerminate(ReadOnlySpan str) Marshal.Release(state); } } + return result; } } diff --git a/src/AzureSignTool/SignCommand.cs b/src/AzureSignTool/SignCommand.cs index 7b53511..3fe4736 100644 --- a/src/AzureSignTool/SignCommand.cs +++ b/src/AzureSignTool/SignCommand.cs @@ -86,6 +86,9 @@ internal sealed class SignCommand [Option("-mdop | --max-degree-of-parallelism", "The maximum number of concurrent signing operations.", CommandOptionType.SingleValue), Range(-1, int.MaxValue)] public int? MaxDegreeOfParallelism { get; set; } + [Option("-st | --signing-throttle", "Controls the rate of signing operations. If this value is specified it indicates the number of seconds to hold off between each signing operation. Only valid when parallelism is disabled.", CommandOptionType.SingleValue), Range(-1, int.MaxValue)] + public int? SigningThrottle { get; set; } + [Option("--colors", "Enable color output on the command line.", CommandOptionType.NoValue)] public bool Colors { get; set; } = false; @@ -96,7 +99,9 @@ internal sealed class SignCommand [Argument(0, "file", "The path to the file.")] public string[] Files { get; set; } = Array.Empty(); + private static DateTime _lastSigningOperation; private HashSet _allFiles; + public HashSet AllFiles { get @@ -152,6 +157,11 @@ private ValidationResult OnValidate(ValidationContext context, CommandLineContex return new ValidationResult("Cannot use '--timestamp-rfc3161' and '--timestamp-authenticode' options together.", new[] { nameof(Rfc3161Timestamp), nameof(AuthenticodeTimestamp) }); } + if (SigningThrottle is > 0 && MaxDegreeOfParallelism > 1) + { + return new ValidationResult("Cannot use '--signing-throttle' and '--max-degree-of-parallelism' options together."); + } + if (KeyVaultClientId.Present && !KeyVaultClientSecret.Present) { return new ValidationResult("Must supply '--azure-key-vault-client-secret' when using '--azure-key-vault-client-id'.", new[] { nameof(KeyVaultClientSecret) }); @@ -169,7 +179,7 @@ private ValidationResult OnValidate(ValidationContext context, CommandLineContex { return new ValidationResult("At least one file must be specified to sign."); } - foreach(var file in AllFiles) + foreach (var file in AllFiles) { if (!File.Exists(file)) { @@ -254,6 +264,13 @@ public async Task OnExecuteAsync(CommandLineApplication app, IConsole conso { performPageHashing = false; } + if (SigningThrottle is > 0) + { + logger?.LogTrace($"Forcing MaxDegreeOfParallelism to 1 because signing throttling is requested."); + MaxDegreeOfParallelism = 1; + _lastSigningOperation = DateTime.Now.AddSeconds(SigningThrottle.Value * -1); + } + var configurationDiscoverer = new KeyVaultConfigurationDiscoverer(logger); var materializedResult = await configurationDiscoverer.Materialize(configuration); AzureKeyVaultMaterializedConfiguration materialized; @@ -279,6 +296,7 @@ public async Task OnExecuteAsync(CommandLineApplication app, IConsole conso { options.MaxDegreeOfParallelism = MaxDegreeOfParallelism.Value; } + logger.LogTrace("Creating context"); using (var keyVault = RSAFactory.Create(materialized.TokenCredential, materialized.KeyId, materialized.PublicCertificate)) @@ -294,6 +312,33 @@ public async Task OnExecuteAsync(CommandLineApplication app, IConsole conso { return state; } + + if (SigningThrottle is > 0) + { + DateTime goTime = _lastSigningOperation.AddSeconds(SigningThrottle.Value); + + if (goTime >= DateTime.Now) + { + logger.LogTrace("Holding off between signing requests until {time}.", goTime); + + while (goTime >= DateTime.Now && !cancellationSource.IsCancellationRequested) + { + Thread.Sleep(500); + } + + logger.LogTrace("Continuing signing operation."); + } + + if (cancellationSource.IsCancellationRequested) + { + pls.Stop(); + } + if (pls.IsStopped) + { + return state; + } + } + using (logger.BeginScope("File: {Id}", filePath)) { logger.LogInformation("Signing file."); @@ -315,6 +360,8 @@ public async Task OnExecuteAsync(CommandLineApplication app, IConsole conso break; } + _lastSigningOperation = DateTime.Now; + if (result == S_OK) { logger.LogInformation("Signing completed successfully.");