Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NuGet.Packaging" Version="7.0.1" />
<PackageVersion Include="NuGet.Protocol" Version="7.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.0" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
</Project>
</Project>
105 changes: 70 additions & 35 deletions src/Sign.Cli/AzureCredentialOptions.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,87 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE.txt file in the project root for more information.

using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using Azure.Core;
using Azure.Identity;

namespace Sign.Cli
{
internal sealed class AzureCredentialOptions
{
internal Option<string?> CredentialTypeOption = new Option<string?>(["--azure-credential-type", "-act"], Resources.CredentialTypeOptionDescription).FromAmong(
AzureCredentialType.AzureCli,
AzureCredentialType.AzurePowerShell,
AzureCredentialType.ManagedIdentity,
AzureCredentialType.WorkloadIdentity);
internal Option<string?> ManagedIdentityClientIdOption = new(["--managed-identity-client-id", "-mici"], Resources.ManagedIdentityClientIdOptionDescription);
internal Option<string?> ManagedIdentityResourceIdOption = new(["--managed-identity-resource-id", "-miri"], Resources.ManagedIdentityResourceIdOptionDescription);
internal Option<bool?> ObsoleteManagedIdentityOption { get; } = new(["--azure-key-vault-managed-identity", "-kvm"], Resources.ManagedIdentityOptionDescription) { IsHidden = true };
internal Option<string?> ObsoleteTenantIdOption { get; } = new(["--azure-key-vault-tenant-id", "-kvt"], Resources.TenantIdOptionDescription) { IsHidden = true };
internal Option<string?> ObsoleteClientIdOption { get; } = new(["--azure-key-vault-client-id", "-kvi"], Resources.ClientIdOptionDescription) { IsHidden = true };
internal Option<string?> ObsoleteClientSecretOption { get; } = new(["--azure-key-vault-client-secret", "-kvs"], Resources.ClientSecretOptionDescription) { IsHidden = true };
internal Option<string?> CredentialTypeOption { get; }
internal Option<string?> ManagedIdentityClientIdOption { get; }
internal Option<string?> ManagedIdentityResourceIdOption { get; }
internal Option<bool?> ObsoleteManagedIdentityOption { get; }
internal Option<string?> ObsoleteTenantIdOption { get; }
internal Option<string?> ObsoleteClientIdOption { get; }
internal Option<string?> ObsoleteClientSecretOption { get; }

internal AzureCredentialOptions()
{
CredentialTypeOption = new Option<string?>("--azure-credential-type", "-act")
{
Description = Resources.CredentialTypeOptionDescription
};
CredentialTypeOption.AcceptOnlyFromAmong(
AzureCredentialType.AzureCli,
AzureCredentialType.AzurePowerShell,
AzureCredentialType.ManagedIdentity,
AzureCredentialType.WorkloadIdentity);

ManagedIdentityClientIdOption = new Option<string?>("--managed-identity-client-id", "-mici")
{
Description = Resources.ManagedIdentityClientIdOptionDescription
};
ManagedIdentityResourceIdOption = new Option<string?>("--managed-identity-resource-id", "-miri")
{
Description = Resources.ManagedIdentityResourceIdOptionDescription
};
ObsoleteManagedIdentityOption = new Option<bool?>("--azure-key-vault-managed-identity", "-kvm")
{
Description = Resources.ManagedIdentityOptionDescription,
Hidden = true
};
ObsoleteTenantIdOption = new Option<string?>("--azure-key-vault-tenant-id", "-kvt")
{
Description = Resources.TenantIdOptionDescription,
Hidden = true
};
ObsoleteClientIdOption = new Option<string?>("--azure-key-vault-client-id", "-kvi")
{
Description = Resources.ClientIdOptionDescription,
Hidden = true
};
ObsoleteClientSecretOption = new Option<string?>("--azure-key-vault-client-secret", "-kvs")
{
Description = Resources.ClientSecretOptionDescription,
Hidden = true
};
}

internal void AddOptionsToCommand(Command command)
{
command.AddOption(CredentialTypeOption);
command.AddOption(ManagedIdentityClientIdOption);
command.AddOption(ManagedIdentityResourceIdOption);
command.AddOption(ObsoleteManagedIdentityOption);
command.AddOption(ObsoleteTenantIdOption);
command.AddOption(ObsoleteClientIdOption);
command.AddOption(ObsoleteClientSecretOption);
command.Options.Add(CredentialTypeOption);
command.Options.Add(ManagedIdentityClientIdOption);
command.Options.Add(ManagedIdentityResourceIdOption);
command.Options.Add(ObsoleteManagedIdentityOption);
command.Options.Add(ObsoleteTenantIdOption);
command.Options.Add(ObsoleteClientIdOption);
command.Options.Add(ObsoleteClientSecretOption);
}

internal DefaultAzureCredentialOptions CreateDefaultAzureCredentialOptions(ParseResult parseResult)
{
DefaultAzureCredentialOptions options = new();

string? managedIdentityClientId = parseResult.GetValueForOption(ManagedIdentityClientIdOption);
string? managedIdentityClientId = parseResult.GetValue(ManagedIdentityClientIdOption);
if (managedIdentityClientId is not null)
{
options.ManagedIdentityClientId = managedIdentityClientId;
}

string? managedIdentityResourceId = parseResult.GetValueForOption(ManagedIdentityResourceIdOption);
string? managedIdentityResourceId = parseResult.GetValue(ManagedIdentityResourceIdOption);
if (managedIdentityResourceId is not null)
{
options.ManagedIdentityResourceId = new ResourceIdentifier(managedIdentityResourceId);
Expand All @@ -55,28 +90,28 @@ internal DefaultAzureCredentialOptions CreateDefaultAzureCredentialOptions(Parse
return options;
}

internal TokenCredential? CreateTokenCredential(InvocationContext context)
internal TokenCredential? CreateTokenCredential(ParseResult parseResult)
{
bool? useManagedIdentity = context.ParseResult.GetValueForOption(ObsoleteManagedIdentityOption);
bool? useManagedIdentity = parseResult.GetValue(ObsoleteManagedIdentityOption);

if (useManagedIdentity is not null)
{
context.Console.Out.WriteLine(Resources.ManagedIdentityOptionObsolete);
Console.Out.WriteLine(Resources.ManagedIdentityOptionObsolete);
}

string? tenantId = context.ParseResult.GetValueForOption(ObsoleteTenantIdOption);
string? clientId = context.ParseResult.GetValueForOption(ObsoleteClientIdOption);
string? secret = context.ParseResult.GetValueForOption(ObsoleteClientSecretOption);
string? tenantId = parseResult.GetValue(ObsoleteTenantIdOption);
string? clientId = parseResult.GetValue(ObsoleteClientIdOption);
string? secret = parseResult.GetValue(ObsoleteClientSecretOption);

if (!string.IsNullOrEmpty(tenantId) &&
!string.IsNullOrEmpty(clientId) &&
!string.IsNullOrEmpty(secret))
{
context.Console.Out.WriteLine(Resources.ClientSecretOptionsObsolete);
Console.Out.WriteLine(Resources.ClientSecretOptionsObsolete);
return new ClientSecretCredential(tenantId, clientId, secret);
}

switch (context.ParseResult.GetValueForOption(CredentialTypeOption))
switch (parseResult.GetValue(CredentialTypeOption))
{
case AzureCredentialType.AzureCli:
return new AzureCliCredential();
Expand All @@ -85,13 +120,13 @@ internal DefaultAzureCredentialOptions CreateDefaultAzureCredentialOptions(Parse
return new AzurePowerShellCredential();

case AzureCredentialType.ManagedIdentity:
string? managedIdentityClientId = context.ParseResult.GetValueForOption(ManagedIdentityClientIdOption);
string? managedIdentityClientId = parseResult.GetValue(ManagedIdentityClientIdOption);
if (managedIdentityClientId is not null)
{
return new ManagedIdentityCredential(managedIdentityClientId);
}

string? managedIdentityResourceId = context.ParseResult.GetValueForOption(ManagedIdentityResourceIdOption);
string? managedIdentityResourceId = parseResult.GetValue(ManagedIdentityResourceIdOption);
if (managedIdentityResourceId is not null)
{
return new ManagedIdentityCredential(new ResourceIdentifier(managedIdentityResourceId));
Expand All @@ -103,7 +138,7 @@ internal DefaultAzureCredentialOptions CreateDefaultAzureCredentialOptions(Parse
return new WorkloadIdentityCredential();

default:
DefaultAzureCredentialOptions options = CreateDefaultAzureCredentialOptions(context.ParseResult);
DefaultAzureCredentialOptions options = CreateDefaultAzureCredentialOptions(parseResult);

// CodeQL [SM05137] Sign CLI is not a production service.
return new DefaultAzureCredential(options);
Expand Down
79 changes: 53 additions & 26 deletions src/Sign.Cli/AzureKeyVaultCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
// See the LICENSE.txt file in the project root for more information.

using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using Azure.Core;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Keys.Cryptography;
Expand All @@ -18,66 +17,80 @@ namespace Sign.Cli
{
internal sealed class AzureKeyVaultCommand : Command
{
internal Option<Uri> UrlOption { get; } = new(["--azure-key-vault-url", "-kvu"], AzureKeyVaultResources.UrlOptionDescription);
internal Option<string> CertificateOption { get; } = new(["--azure-key-vault-certificate", "-kvc"], AzureKeyVaultResources.CertificateOptionDescription);
internal Option<Uri> UrlOption { get; }
internal Option<string> CertificateOption { get; }
internal AzureCredentialOptions AzureCredentialOptions { get; } = new();

internal Argument<List<string>?> FilesArgument { get; } = new("file(s)", Resources.FilesArgumentDescription) { Arity = ArgumentArity.OneOrMore };
internal Argument<List<string>?> FilesArgument { get; }

internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory serviceProviderFactory)
: base("azure-key-vault", AzureKeyVaultResources.CommandDescription)
{
ArgumentNullException.ThrowIfNull(codeCommand, nameof(codeCommand));
ArgumentNullException.ThrowIfNull(serviceProviderFactory, nameof(serviceProviderFactory));

CertificateOption.IsRequired = true;
UrlOption.IsRequired = true;
UrlOption = new Option<Uri>("--azure-key-vault-url", "-kvu")
{
Description = AzureKeyVaultResources.UrlOptionDescription,
Required = true,
CustomParser = ParseUrl
};
CertificateOption = new Option<string>("--azure-key-vault-certificate", "-kvc")
{
Description = AzureKeyVaultResources.CertificateOptionDescription,
Required = true
};
FilesArgument = new Argument<List<string>?>("file(s)")
{
Description = Resources.FilesArgumentDescription,
Arity = ArgumentArity.OneOrMore
};

AddOption(UrlOption);
AddOption(CertificateOption);
Options.Add(UrlOption);
Options.Add(CertificateOption);
AzureCredentialOptions.AddOptionsToCommand(this);

AddArgument(FilesArgument);
Arguments.Add(FilesArgument);

this.SetHandler(async (InvocationContext context) =>
SetAction((ParseResult parseResult, CancellationToken cancellationToken) =>
{
List<string>? filesArgument = context.ParseResult.GetValueForArgument(FilesArgument);
List<string>? filesArgument = parseResult.GetValue(FilesArgument);

if (filesArgument is not { Count: > 0 })
{
context.Console.Error.WriteLine(Resources.MissingFileValue);
context.ExitCode = ExitCode.InvalidOptions;
return;
Console.Error.WriteLine(Resources.MissingFileValue);

return Task.FromResult(ExitCode.InvalidOptions);
}

// this check exists as a courtesy to users who may have been signing .clickonce files via the old workaround.
// at some point we should remove this check, probably once we hit v1.0
if (filesArgument.Any(x => x.EndsWith(".clickonce", StringComparison.OrdinalIgnoreCase)))
{
context.Console.Error.WriteLine(AzureKeyVaultResources.ClickOnceExtensionNotSupported);
context.ExitCode = ExitCode.InvalidOptions;
return;
Console.Error.WriteLine(AzureKeyVaultResources.ClickOnceExtensionNotSupported);

return Task.FromResult(ExitCode.InvalidOptions);
}

TokenCredential? credential = AzureCredentialOptions.CreateTokenCredential(context);
TokenCredential? credential = AzureCredentialOptions.CreateTokenCredential(parseResult);
if (credential is null)
{
return;
return Task.FromResult(ExitCode.Failed);
}

// Some of the options are required and that is why we can safely use
// the null-forgiving operator (!) to simplify the code.
Uri url = context.ParseResult.GetValueForOption(UrlOption)!;
string certificateId = context.ParseResult.GetValueForOption(CertificateOption)!;
Uri url = parseResult.GetValue(UrlOption)!;
string certificateId = parseResult.GetValue(CertificateOption)!;

// Construct the URI for the certificate and the key from user parameters. We'll validate those with the SDK
var certUri = new Uri($"{url.Scheme}://{url.Authority}/certificates/{certificateId}");

if (!KeyVaultCertificateIdentifier.TryCreate(certUri, out var certId))
{
context.Console.Error.WriteLine(AzureKeyVaultResources.InvalidKeyVaultUrl);
context.ExitCode = ExitCode.InvalidOptions;
return;
Console.Error.WriteLine(AzureKeyVaultResources.InvalidKeyVaultUrl);

return Task.FromResult(ExitCode.InvalidOptions);
}

// The key uri is similar and the key name matches the certificate name
Expand Down Expand Up @@ -105,8 +118,22 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s

KeyVaultServiceProvider keyVaultServiceProvider = new();

await codeCommand.HandleAsync(context, serviceProviderFactory, keyVaultServiceProvider, filesArgument);
return codeCommand.HandleAsync(parseResult, serviceProviderFactory, keyVaultServiceProvider, filesArgument);
});
}

private static Uri? ParseUrl(ArgumentResult result)
{
if (result.Tokens.Count != 1 ||
!Uri.TryCreate(result.Tokens[0].Value, UriKind.Absolute, out Uri? uri)
|| !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
result.AddError(AzureKeyVaultResources.InvalidUrlValue);

return null;
}

return uri;
}
}
}
11 changes: 10 additions & 1 deletion src/Sign.Cli/AzureKeyVaultResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Sign.Cli/AzureKeyVaultResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
<data name="InvalidKeyVaultUrl" xml:space="preserve">
<value>URL must only contain the protocol and host. (e.g.: https://&lt;vault-name&gt;.vault.azure.net/)</value>
</data>
<data name="InvalidUrlValue" xml:space="preserve">
<value>URL must be an absolute HTTPS URL to an Azure Key Vault.</value>
</data>
<data name="UrlOptionDescription" xml:space="preserve">
<value>URL to an Azure Key Vault.</value>
</data>
Expand Down
Loading