Skip to content
Open
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ The single-file downloads do not require .NET to be installed on the system at a
Azure, which will be used to generate an access token. This parameter is not required if an access token is supplied
directly with the `--azure-key-vault-accesstoken` option or when using managed identities with `--azure-key-vault-managed-identity`. If this parameter is supplied, `--azure-key-vault-client-id` and `--azure-key-vault-tenant-id` must be supplied as well.

* `--azure-key-vault-client-auth-certificate` [short: `-kvac`, required: possibly]: This defines the thumbprint of a client authentication certificate, which is used to generate an access token for authentication to Azure. This parameter is not required if an access token is supplied
directly with the `--azure-key-vault-accesstoken` option or when using managed identities with `--azure-key-vault-managed-identity`.
If this parameter is supplied, `--azure-key-vault-client-id` and `--azure-key-vault-tenant-id` must be supplied as well and `--azure-key-vault-client-secret` must not be used.
Instead of using a secret a certificate is reuqired installed on the build machine executing the AzureSignTool and the public key must be known in the
Azure Key Valut. The Thumbprint of the certificate is used here.
This options allows more control which computer can sign and use the codesigning certificate, because it does not depend on the knowledge of the secret in the build pipeline.

* `--azure-key-vault-tenant-id` [short: `-kvt`, required: possibly]: This is the tenant id used to authenticate to
Azure, which will be used to generate an access token. This parameter is not required if an access token is supplied
directly with the `--azure-key-vault-accesstoken` option or when using managed identities with `--azure-key-vault-managed-identity`. If this parameter is supplied, `--azure-key-vault-client-id` and `--azure-key-vault-client-secret` must be supplied as well.
Expand Down
1 change: 1 addition & 0 deletions src/AzureSignTool/AzureKeyVaultSignConfigurationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ public sealed class AzureKeyVaultSignConfigurationSet
public string AzureKeyVaultCertificateVersion { get; init; }
public string AzureAccessToken { get; init; }
public string AzureAuthority { get; init; }
public string AzureCertificateThumbprint { get; set; }
}
}
39 changes: 39 additions & 0 deletions src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#nullable enable
using Azure.Core;
using Azure.Identity;
using Azure.Security.KeyVault.Certificates;

using Microsoft.Extensions.Logging;
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace AzureSignTool
{

internal class KeyVaultConfigurationDiscoverer
{
private readonly ILogger _logger;
Expand All @@ -29,6 +32,14 @@ public async Task<ErrorOr<AzureKeyVaultMaterializedConfiguration>> Materialize(A
{
credential = new AccessTokenCredential(configuration.AzureAccessToken);
}
else if (!string.IsNullOrWhiteSpace(configuration.AzureCertificateThumbprint))
{
string certificateThumbPrint = configuration.AzureCertificateThumbprint;
X509Certificate2? clientCertificate = LoadCertificateByThumbprint(certificateThumbPrint, StoreLocation.CurrentUser);
clientCertificate ??= LoadCertificateByThumbprint(certificateThumbPrint, StoreLocation.LocalMachine);

credential = new ClientCertificateCredential(configuration.AzureTenantId, configuration.AzureClientId, clientCertificate);
}
else
{
if (string.IsNullOrWhiteSpace(configuration.AzureAuthority))
Expand Down Expand Up @@ -82,5 +93,33 @@ public async Task<ErrorOr<AzureKeyVaultMaterializedConfiguration>> Materialize(A

return new AzureKeyVaultMaterializedConfiguration(credential, certificate, keyId);
}

private X509Certificate2? LoadCertificateByThumbprint(string thumbprint, StoreLocation storeLocation)
{
X509Store certStore = new X509Store(StoreName.My, storeLocation);
certStore.Open(OpenFlags.ReadOnly);
try
{
X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
Comment thread
vcsjones marked this conversation as resolved.
if (certCollection.Count > 0)
{
X509Certificate2 cert = certCollection[0];
return cert;
}
else
{
_logger.LogTrace($"Could not find certificate with thumbprint {thumbprint} in store {storeLocation}");
return null;
}
}
catch (CryptographicException)
{
return null;
}
finally
Comment thread
AsmCord marked this conversation as resolved.
{
certStore.Close();
}
}
}
}
36 changes: 34 additions & 2 deletions src/AzureSignTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal sealed class SignCommand : Command
internal string? KeyVaultUrl { get; set; }
internal string? KeyVaultClientId { get; set; }
internal string? KeyVaultClientSecret { get; set; }
internal string? KeyVaultClientAuthCertificate { get; set; }
internal string? KeyVaultTenantId { get; set; }
internal string? KeyVaultCertificate { get; set; }
internal string? KeyVaultCertificateVersion { get; set; }
Expand Down Expand Up @@ -154,6 +155,7 @@ public SignCommand() : base("sign", "Sign a file.", null)
this.Add("kvu|azure-key-vault-url=", "The {URL} to an Azure Key Vault.", v => KeyVaultUrl = v);
this.Add("kvi|azure-key-vault-client-id=", "The Client {ID} to authenticate to the Azure Key Vault.", v => KeyVaultClientId = v);
this.Add("kvs|azure-key-vault-client-secret=", "The Client Secret to authenticate to the Azure Key Vault.", v => KeyVaultClientSecret = v);
this.Add("kvac|azure-key-vault-client-auth-certificate=", "The Client certificate thumbprint to authenticate to the Azure Key Vault.", v => KeyVaultClientAuthCertificate = v);
this.Add("kvt|azure-key-vault-tenant-id=", "The Tenant Id to authenticate to the Azure Key Vault.", v => KeyVaultTenantId = v);
this.Add("kvc|azure-key-vault-certificate=", "The name of the certificate in Azure Key Vault.", v => KeyVaultCertificate = v);
this.Add("kvcv|azure-key-vault-certificate-version=", "The version of the certificate in Azure Key Vault to use. The current version of the certificate is used by default.", v => KeyVaultCertificateVersion = v);
Expand Down Expand Up @@ -236,6 +238,7 @@ private async ValueTask<int> RunSign(HashSet<string> allFiles)
AzureClientSecret = KeyVaultClientSecret,
ManagedIdentity = UseManagedIdentity,
AzureAuthority = AzureAuthority,
AzureCertificateThumbprint = KeyVaultClientAuthCertificate
};

TimeStampConfiguration timeStampConfiguration;
Expand Down Expand Up @@ -438,9 +441,15 @@ private bool ValidateArguments(CommandRunContext context)
valid = false;
}

if (KeyVaultClientId is not null && KeyVaultClientSecret is null)
if (KeyVaultClientId is not null && KeyVaultClientSecret is null && KeyVaultClientAuthCertificate is null)
Comment thread
AsmCord marked this conversation as resolved.
{
context.Error.WriteLine("Must supply '--azure-key-vault-client-secret' when using '--azure-key-vault-client-id'.");
context.Error.WriteLine("Must supply '--azure-key-vault-client-secret' or '--azure-key-vault-client-auth-certificate' when using '--azure-key-vault-client-id'.");
valid = false;
}

if (KeyVaultClientId is not null && KeyVaultClientAuthCertificate is not null && !IsValidHex(KeyVaultClientAuthCertificate))
{
context.Error.WriteLine("The value for '--azure-key-vault-client-auth-certificate' must be a valid hexadecimal string when using '--azure-key-vault-client-id'.");
valid = false;
}

Expand Down Expand Up @@ -516,6 +525,29 @@ private static bool ValidateFiles(CommandRunContext context, HashSet<string> all
return valid;
}

static bool IsValidHex(string input)
{
if (input is not { Length: 40 })
{
return false;
}

foreach (char c in input)
{
switch (c)
{
case >= 'a' and <= 'f':
case >= 'A' and <= 'F':
case >= '0' and <= '9':
continue;
default:
return false;
}
}

return true;
}

private static bool ValidateHashAlgorithm(CommandRunContext context, string? input, string optionName)
{
if (input is null)
Expand Down