diff --git a/README.md b/README.md index 41a392a..be1416b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/AzureSignTool/AzureKeyVaultSignConfigurationSet.cs b/src/AzureSignTool/AzureKeyVaultSignConfigurationSet.cs index b38d869..092016d 100644 --- a/src/AzureSignTool/AzureKeyVaultSignConfigurationSet.cs +++ b/src/AzureSignTool/AzureKeyVaultSignConfigurationSet.cs @@ -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; } } } diff --git a/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs b/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs index 1c9f0af..1681ecf 100644 --- a/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs +++ b/src/AzureSignTool/KeyVaultConfigurationDiscoverer.cs @@ -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; @@ -29,6 +32,14 @@ public async Task> 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)) @@ -82,5 +93,33 @@ public async Task> 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); + 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 + { + certStore.Close(); + } + } } } diff --git a/src/AzureSignTool/Program.cs b/src/AzureSignTool/Program.cs index 7bc1e12..b9a6e90 100644 --- a/src/AzureSignTool/Program.cs +++ b/src/AzureSignTool/Program.cs @@ -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; } @@ -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); @@ -236,6 +238,7 @@ private async ValueTask RunSign(HashSet allFiles) AzureClientSecret = KeyVaultClientSecret, ManagedIdentity = UseManagedIdentity, AzureAuthority = AzureAuthority, + AzureCertificateThumbprint = KeyVaultClientAuthCertificate }; TimeStampConfiguration timeStampConfiguration; @@ -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) { - 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; } @@ -516,6 +525,29 @@ private static bool ValidateFiles(CommandRunContext context, HashSet 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)