From 9c73c6f914b3d25689aab7a023e3daf436d6a4c4 Mon Sep 17 00:00:00 2001 From: Oliver Nelson Date: Fri, 16 Jan 2026 23:11:03 +0000 Subject: [PATCH] Add configurable timeout for Azure Key Vault connections - Add --azure-key-vault-timeout parameter (default: 30 seconds) - Return standard Windows ERROR_TIMEOUT (0x800705B5) on timeout - Prevent long waits when Key Vault URL is unreachable - Use CancellationTokenSource to enforce timeout on certificate retrieval operations Resolves issue where the program would wait several minutes when Key Vault URL cannot be reached. --- src/AzureSignTool/HRESULT.cs | 1 + .../KeyVaultConfigurationDiscoverer.cs | 47 +++++++++++-------- src/AzureSignTool/Program.cs | 13 ++++- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/AzureSignTool/HRESULT.cs b/src/AzureSignTool/HRESULT.cs index 3e6d289..8bbe3fd 100644 --- a/src/AzureSignTool/HRESULT.cs +++ b/src/AzureSignTool/HRESULT.cs @@ -12,5 +12,6 @@ internal static class HRESULT public const int E_ALL_FAILED = unchecked((int)0xA0000002); public const int TRUST_E_SUBJECT_FORM_UNKNOWN = unchecked((int)0x800B0003); + public const int ERROR_TIMEOUT = unchecked((int)0x800705B5); } } diff --git a/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs b/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs index 1c9f0af..a2cd86b 100644 --- a/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs +++ b/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; namespace AzureSignTool @@ -18,7 +19,7 @@ public KeyVaultConfigurationDiscoverer(ILogger logger) _logger = logger; } - public async Task> Materialize(AzureKeyVaultSignConfigurationSet configuration) + public async Task> Materialize(AzureKeyVaultSignConfigurationSet configuration, int timeoutSeconds) { TokenCredential credential; if (configuration.ManagedIdentity) @@ -48,30 +49,38 @@ public async Task> Materialize(A X509Certificate2 certificate; KeyVaultCertificate azureCertificate; - try + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) { - var certClient = new CertificateClient(configuration.AzureKeyVaultUrl, credential); - - if (!string.IsNullOrWhiteSpace(configuration.AzureKeyVaultCertificateVersion)) + try { - _logger.LogTrace($"Retrieving version [{configuration.AzureKeyVaultCertificateVersion}] of certificate {configuration.AzureKeyVaultCertificateName}."); - azureCertificate = (await certClient.GetCertificateVersionAsync(configuration.AzureKeyVaultCertificateName, configuration.AzureKeyVaultCertificateVersion).ConfigureAwait(false)).Value; + var certClient = new CertificateClient(configuration.AzureKeyVaultUrl, credential); + + if (!string.IsNullOrWhiteSpace(configuration.AzureKeyVaultCertificateVersion)) + { + _logger.LogTrace($"Retrieving version [{configuration.AzureKeyVaultCertificateVersion}] of certificate {configuration.AzureKeyVaultCertificateName}."); + azureCertificate = (await certClient.GetCertificateVersionAsync(configuration.AzureKeyVaultCertificateName, configuration.AzureKeyVaultCertificateVersion, cts.Token).ConfigureAwait(false)).Value; + } + else + { + _logger.LogTrace($"Retrieving current version of certificate {configuration.AzureKeyVaultCertificateName}."); + azureCertificate = (await certClient.GetCertificateAsync(configuration.AzureKeyVaultCertificateName, cts.Token).ConfigureAwait(false)).Value; + } + _logger.LogTrace($"Retrieved certificate with Id {azureCertificate.Id}."); + + certificate = X509CertificateLoader.LoadCertificate(azureCertificate.Cer); } - else + catch (OperationCanceledException) { - _logger.LogTrace($"Retrieving current version of certificate {configuration.AzureKeyVaultCertificateName}."); - azureCertificate = (await certClient.GetCertificateAsync(configuration.AzureKeyVaultCertificateName).ConfigureAwait(false)).Value; + _logger.LogError($"Timeout connecting to Azure Key Vault at {configuration.AzureKeyVaultUrl}. The operation exceeded {timeoutSeconds} seconds."); + return new TimeoutException($"The connection to Azure Key Vault timed out after {timeoutSeconds} seconds."); } - _logger.LogTrace($"Retrieved certificate with Id {azureCertificate.Id}."); - - certificate = X509CertificateLoader.LoadCertificate(azureCertificate.Cer); - } - catch (Exception e) - { - _logger.LogError($"Failed to retrieve certificate {configuration.AzureKeyVaultCertificateName} from Azure Key Vault. Please verify the name of the certificate and the permissions to the certificate. Error message: {e.Message}."); - _logger.LogTrace(e.ToString()); + catch (Exception e) + { + _logger.LogError($"Failed to retrieve certificate {configuration.AzureKeyVaultCertificateName} from Azure Key Vault. Please verify the name of the certificate and the permissions to the certificate. Error message: {e.Message}."); + _logger.LogTrace(e.ToString()); - return e; + return e; + } } var keyId = azureCertificate.KeyId; diff --git a/src/AzureSignTool/Program.cs b/src/AzureSignTool/Program.cs index ecc397c..4bae1f9 100644 --- a/src/AzureSignTool/Program.cs +++ b/src/AzureSignTool/Program.cs @@ -91,6 +91,7 @@ internal sealed class SignCommand : Command internal bool SkipSignedFiles { get; set; } internal bool AppendSignature { get; set; } internal string? AzureAuthority { get; set; } + internal int KeyVaultTimeoutSeconds { get; set; } = 30; internal HashSet AllFiles { @@ -185,6 +186,7 @@ public SignCommand() : base("sign", "Sign a file.", null) this.Add("s|skip-signed", "Skip files that are already signed.", v => SkipSignedFiles = v is not null); this.Add("as|append-signature", "Append the signature, has no effect with --skip-signed.", v => AppendSignature = v is not null); this.Add("au|azure-authority=", "The Azure Authority for Azure Key Vault.", v => AzureAuthority = v); + this.Add("kvto|azure-key-vault-timeout=", "The timeout in seconds for connecting to Azure Key Vault. Default is 30 seconds.", (int v) => KeyVaultTimeoutSeconds = v); this.Add("<>", "[files]*", Files); Action = Run; } @@ -263,7 +265,7 @@ private async ValueTask RunSign() } bool appendSignature = AppendSignature; var configurationDiscoverer = new KeyVaultConfigurationDiscoverer(logger); - var materializedResult = await configurationDiscoverer.Materialize(configuration); + var materializedResult = await configurationDiscoverer.Materialize(configuration, KeyVaultTimeoutSeconds); AzureKeyVaultMaterializedConfiguration materialized; switch (materializedResult) @@ -271,6 +273,9 @@ private async ValueTask RunSign() case ErrorOr.Ok ok: materialized = ok.Value; break; + case ErrorOr.Err err when err.Error is TimeoutException: + logger.LogError("Timeout connecting to Azure Key Vault. The Key Vault URL could not be reached within {timeout} seconds.", KeyVaultTimeoutSeconds); + return ERROR_TIMEOUT; default: logger.LogError("Failed to get configuration from Azure Key Vault."); return E_INVALIDARG; @@ -487,6 +492,12 @@ private bool ValidateArguments(CommandRunContext context) valid = false; } + if (KeyVaultTimeoutSeconds <= 0) + { + context.Error.WriteLine("'--azure-key-vault-timeout' must be a positive integer."); + valid = false; + } + if (AllFiles.Count == 0) { context.Error.WriteLine("At least one file must be specified to sign.");