Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 13 additions & 11 deletions src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace AzureSign.Core
{
Expand All @@ -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;

/// <summary>
/// Creates a new instance of <see cref="AuthenticodeKeyVaultSigner" />.
/// </summary>
/// <param name="signingAlgorithm">
/// 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.
/// </param>
/// <param name="signingCertificate">The X509 public certificate for the <paramref name="signingAlgorithm"/>.</param>
/// <param name="fileDigestAlgorithm">The digest algorithm to sign the file.</param>
/// <param name="timeStampConfiguration">The timestamp configuration for timestamping the file. To omit timestamping,
/// use <see cref="TimeStampConfiguration.None"/>.</param>
/// use <see cref="TimeStampConfiguration.None"/>.</param>
/// <param name="additionalCertificates">Any additional certificates to assist in building a certificate chain.</param>
public AuthenticodeKeyVaultSigner(AsymmetricAlgorithm signingAlgorithm, X509Certificate2 signingCertificate,
HashAlgorithmName fileDigestAlgorithm, TimeStampConfiguration timeStampConfiguration,
Expand Down Expand Up @@ -66,16 +66,17 @@ public AuthenticodeKeyVaultSigner(AsymmetricAlgorithm signingAlgorithm, X509Cert
}

/// <summary>Authenticode signs a file.</summary>
/// <param name="pageHashing">True if the signing process should try to include page hashing, otherwise false.
/// Use <c>null</c> to use the operating system default. Note that page hashing still may be disabled if the
/// Subject Interface Package does not support page hashing.</param>
/// <param name="descriptionUrl">A URL describing the signature or the signer.</param>
/// <param name="description">The description to apply to the signature.</param>
/// <param name="path">The path to the file to signed.</param>
/// <param name="description">The description to apply to the signature.</param>
/// <param name="descriptionUrl">A URL describing the signature or the signer.</param>
/// <param name="pageHashing">True if the signing process should try to include page hashing, otherwise false.
/// Use <c>null</c> to use the operating system default. Note that page hashing still may be disabled if the
/// Subject Interface Package does not support page hashing.</param>
/// <param name="logger">An optional logger to capture signing operations.</param>
/// <returns>A HRESULT indicating the result of the signing operation. S_OK, or zero, is returned if the signing
/// operation completed successfully.</returns>
public unsafe int SignFile(ReadOnlySpan<char> path, ReadOnlySpan<char> description, ReadOnlySpan<char> descriptionUrl, bool? pageHashing, ILogger? logger = null)
public unsafe int SignFile(ReadOnlySpan<char> path, ReadOnlySpan<char> description,
ReadOnlySpan<char> descriptionUrl, bool? pageHashing, ILogger? logger = null)
{
static char[] NullTerminate(ReadOnlySpan<char> str)
{
Expand Down Expand Up @@ -197,6 +198,7 @@ static char[] NullTerminate(ReadOnlySpan<char> str)
Marshal.Release(state);
}
}

return result;
}
}
Expand Down
49 changes: 48 additions & 1 deletion src/AzureSignTool/SignCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -96,7 +99,9 @@ internal sealed class SignCommand
[Argument(0, "file", "The path to the file.")]
public string[] Files { get; set; } = Array.Empty<string>();

private static DateTime _lastSigningOperation;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be static? Does it even need to be a field, or can it be part of the state in Parallel.ForEach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while since I wrote this and I'll have to double check whether throttling limits it you to a single request thread. I have a feeling I made it so that it did; if I did then there's not really any need for it to be static and I'll pull it in to the state.

private HashSet<string> _allFiles;

public HashSet<string> AllFiles
{
get
Expand Down Expand Up @@ -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) });
Expand All @@ -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))
{
Expand Down Expand Up @@ -254,6 +264,13 @@ public async Task<int> 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;
Expand All @@ -279,6 +296,7 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole conso
{
options.MaxDegreeOfParallelism = MaxDegreeOfParallelism.Value;
}

logger.LogTrace("Creating context");

using (var keyVault = RSAFactory.Create(materialized.TokenCredential, materialized.KeyId, materialized.PublicCertificate))
Expand All @@ -294,6 +312,33 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole conso
{
return state;
}

if (SigningThrottle is > 0)
{
DateTime goTime = _lastSigningOperation.AddSeconds(SigningThrottle.Value);

if (goTime >= DateTime.Now)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some concerns around this since DateTime.Now is not monotonic - for example, a Daylight Saving Time. Let's say the _lastSigningOperation is at 1:59 AM. Day Light Savings kicks in at 2:00 AM, and jumps forward to 3:00 AM. This will violate the throttle.

Using DateTimeOffset.UtcNow probably mitigates most of those concerns... except for things like clock correction. I think that is a tradeoff I am willing to accept. A "true" monotonic clock source is probably overkill for this purpose.

Can we re-work this to use DateTimeOffset.UtcNow?

Copy link
Contributor Author

@hikariuk hikariuk Aug 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could possibly use System.Threading.PeriodicTimer instead, which was added at some point, but I'd need to check the version availability for it. You just call the wait method on the class; it'll return immediately if there's already been one or more ticks since the method was last called, or it will wait until there is one and then return.

{
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.");
Expand All @@ -315,6 +360,8 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole conso
break;
}

_lastSigningOperation = DateTime.Now;

if (result == S_OK)
{
logger.LogInformation("Signing completed successfully.");
Expand Down