diff --git a/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs
new file mode 100644
index 0000000000..93ce21bd86
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs
@@ -0,0 +1,368 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Dependencies
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.IO.Abstractions;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.DependencyInjection;
+ using Polly;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Configuration;
+ using VirtualClient.Contracts;
+ using VirtualClient.Logging;
+
+ ///
+ /// Provides functionality for downloading and installing dependency certificates from
+ /// a cloud keyvault store onto the system.
+ ///
+ public class DependencyCertificateInstallation : VirtualClientComponent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides all of the required dependencies to the Virtual Client component.
+ ///
+ /// Parameters defined in the execution profile or supplied to the Virtual Client on the command line.
+ ///
+ public DependencyCertificateInstallation(IServiceCollection dependencies, IDictionary parameters = null)
+ : base(dependencies, parameters)
+ {
+ }
+
+ ///
+ /// The type of archival file format the blob is in.
+ ///
+ public ArchiveType ArchiveType
+ {
+ get
+ {
+ return this.Parameters.GetEnumValue(nameof(DependencyCertificateInstallation.ArchiveType), ArchiveType.Zip);
+ }
+ }
+
+ ///
+ /// The name of the blob package in the storage account.
+ ///
+ public string CertificateName
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.CertificateName));
+ }
+
+ set
+ {
+ this.Parameters[nameof(DependencyCertificateInstallation.CertificateName)] = value;
+ }
+ }
+
+ ///
+ /// The name of the container in which the blob package exists in
+ /// the storage account.
+ ///
+ public string KeyVaultName
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.KeyVaultName));
+ }
+
+ set
+ {
+ this.Parameters[nameof(DependencyCertificateInstallation.KeyVaultName)] = value;
+ }
+ }
+
+ ///
+ /// The path in which to install the package.
+ ///
+ public string InstallationPath
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.InstallationPath), string.Empty);
+ }
+
+ set
+ {
+ this.Parameters[nameof(DependencyCertificateInstallation.InstallationPath)] = value;
+ }
+ }
+
+ ///
+ /// Whether the blob should be extracted.
+ ///
+ public bool Extract
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.Extract), true);
+ }
+
+ set
+ {
+ this.Parameters[nameof(DependencyCertificateInstallation.Extract)] = value;
+ }
+ }
+
+ ///
+ /// Defines an access token to use for authentication + authorization with
+ /// Azure resources.
+ ///
+ public string AccessToken { get; set; }
+
+ ///
+ /// Flag indicates whether the installer is running unattended. This scenario
+ /// is used with installations via other automation to ensure the installer does
+ /// not block on exiting.
+ ///
+ public bool Unattended { get; set; }
+
+ ///
+ /// A retry policy to apply to transient issues with accessing secrets in
+ /// an Azure Key Vault.
+ ///
+ protected IAsyncPolicy KeyVaultAccessRetryPolicy { get; set; }
+
+ ///
+ /// Executes the blob package download/installation operation.
+ ///
+ protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ IPackageManager packageManager = this.Dependencies.GetService();
+ IFileSystem fileSystem = this.Dependencies.GetService();
+
+ DependencyPath existingPackage = await packageManager.GetPackageAsync(this.PackageName, cancellationToken)
+ .ConfigureAwait(false);
+
+ /*
+ MetadataContract.Persist(
+ $"package_{this.PackageName}",
+ this.BlobName,
+ MetadataContract.DependenciesCategory,
+ true);
+
+ IPackageManager packageManager = this.Dependencies.GetService();
+ IFileSystem fileSystem = this.Dependencies.GetService();
+
+ DependencyPath existingPackage = await packageManager.GetPackageAsync(this.PackageName, cancellationToken)
+ .ConfigureAwait(false);
+
+ telemetryContext.AddContext("packageDirectory", this.PlatformSpecifics.GetPackagePath());
+ telemetryContext.AddContext("packageExists", existingPackage != null);
+ telemetryContext.AddContext("package", existingPackage);
+
+ // If a built-in package exists, we do not currently override it with a Blob package
+ // downloaded to the system.
+ if (existingPackage == null || !fileSystem.Directory.Exists(existingPackage.Path))
+ {
+ BlobDescriptor packageDescription = new BlobDescriptor
+ {
+ Name = this.BlobName,
+ ContainerName = this.BlobContainer,
+ PackageName = this.PackageName,
+ ArchiveType = this.ArchiveType,
+ Extract = this.Extract
+ };
+
+ telemetryContext.AddContext("package", packageDescription);
+
+ string installationPath = null;
+ if (!string.IsNullOrWhiteSpace(this.InstallationPath))
+ {
+ IDiskManager diskManager = this.Dependencies.GetService();
+ IEnumerable disks = await diskManager.GetDisksAsync(cancellationToken).ConfigureAwait(false);
+ if (!DependencyCertificateInstallation.TryResolveRelativeDiskLocation(disks, this.InstallationPath, this.Platform, out installationPath))
+ {
+ throw new WorkloadException(
+ $"The installation path provided '{this.InstallationPath}' cannot be resolved",
+ ErrorReason.DependencyInstallationFailed);
+ }
+ }
+
+ if (!this.TryGetPackageStoreManager(out IBlobManager blobManager))
+ {
+ throw new DependencyException(
+ $"Package store not defined. The package '{packageDescription.Name}' cannot be installed because the package store information " +
+ $"was not provided to the application on the command line (e.g. --packages).",
+ ErrorReason.PackageStoreNotDefined);
+ }
+
+ string packageLocation = await packageManager.InstallPackageAsync(blobManager, packageDescription, cancellationToken, installationPath)
+ .ConfigureAwait(false);
+
+ if (string.IsNullOrWhiteSpace(packageLocation))
+ {
+ throw new DependencyException(
+ $"Blob package installation failed for package '{this.BlobName}'.",
+ ErrorReason.DependencyInstallationFailed);
+ }
+
+ telemetryContext.AddContext("packageLocation", packageLocation);
+ }
+
+ */
+ }
+
+ ///
+ /// Downloads the dependency from the container specified to the path defined.
+ ///
+ /// The blob manager to use for downloading the package dependency.
+ /// The dependency description.
+ /// Provides the location where the package should be downloaded.
+ /// A token that can be used to cancel the operation.
+ protected virtual async Task DownloadDependencyPackageAsync(IBlobManager blobManager, DependencyDescriptor description, string downloadPath, CancellationToken cancellationToken)
+ {
+ FileStream stream = new FileStream(downloadPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
+
+ await blobManager.DownloadBlobAsync(description, stream, cancellationToken)
+ .ConfigureAwait(false);
+
+ return stream;
+ }
+
+ ///
+ /// Installs certificates required by the CRC SDK on the local system.
+ ///
+ /// Provides dependencies required to download and install certificates.
+ /// A token that can be used to cancel the operation.
+ protected async Task InstallCertificatesAsync(IServiceCollection dependencies, CancellationToken cancellationToken)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"[Downloading Certificates]");
+ // Console.WriteLine($"Certificate = {settings.InstallerCertificateName}");
+
+ PlatformSpecifics platformSpecifics = dependencies.GetService();
+ PlatformID platform = platformSpecifics.Platform;
+
+ X509Certificate2 certificate = null; // await this.DownloadCertificateAsync(settings, platform, cancellationToken);
+
+ try
+ {
+ Console.WriteLine();
+ Console.WriteLine($"[Installing Certificates]");
+
+ if (platform == PlatformID.Unix)
+ {
+ await this.InstallCertificateOnUnixAsync(certificate, dependencies, cancellationToken);
+ }
+ else if (platform == PlatformID.Win32NT)
+ {
+ await this.InstallCertificateOnWindowsAsync(certificate, cancellationToken);
+ }
+ else
+ {
+ throw new DependencyException(
+ $"Certificate installation for OS platform '{platform}' is not supported.",
+ ErrorReason.NotSupported);
+ }
+ }
+ catch (CryptographicException exc) when (exc.Message.Contains("access", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new DependencyException(
+ $"Certificate installation failed. Local certificate store access permissions denied. The CRC SDK installer must be " +
+ $"run with Administrative privileges in order to install certificates in the current context.",
+ ErrorReason.DependencyInstallationFailed);
+ }
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Windows system.
+ ///
+ protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
+ {
+ return Task.Run(() =>
+ {
+ Console.WriteLine($"Certificate Store = CurrentUser/Personal");
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+ });
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Unix/Linux system.
+ ///
+ protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, IServiceCollection dependencies, CancellationToken cancellationToken)
+ {
+ PlatformSpecifics platformSpecifics = dependencies.GetService();
+ ProcessManager processManager = dependencies.GetService();
+ IFileSystem fileSystem = dependencies.GetService();
+
+ // On Unix/Linux systems, we install ther certificate in the default location for the
+ // user as well as in a static location. In the future we will likely use the static location
+ // only.
+ string certificateDirectory = null;
+
+ try
+ {
+ // When "sudo" is used to run the installer, we need to know the logged
+ // in user account. On Linux systems, there is an environment variable 'SUDO_USER'
+ // that defines the logged in user.
+ string user = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.USER);
+ string sudoUser = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.SUDO_USER);
+ certificateDirectory = $"/home/{user}/.dotnet/corefx/cryptography/x509stores/my";
+
+ if (!string.IsNullOrWhiteSpace(sudoUser))
+ {
+ // The installer is being executed with "sudo" privileges. We want to use the
+ // logged in user profile vs. "root".
+ certificateDirectory = $"/home/{sudoUser}/.dotnet/corefx/cryptography/x509stores/my";
+ }
+ else if (user == "root")
+ {
+ // The installer is being executed from the "root" account on Linux.
+ certificateDirectory = $"/root/.dotnet/corefx/cryptography/x509stores/my";
+ }
+
+ Console.WriteLine($"Certificate Store = {certificateDirectory}");
+
+ if (!fileSystem.Directory.Exists(certificateDirectory))
+ {
+ fileSystem.Directory.CreateDirectory(certificateDirectory);
+ }
+
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+
+ await fileSystem.File.WriteAllBytesAsync(
+ platformSpecifics.Combine(certificateDirectory, $"{certificate.Thumbprint}.pfx"),
+ certificate.Export(X509ContentType.Pfx));
+
+ // Permissions 777 (-rwxrwxrwx)
+ // https://linuxhandbook.com/linux-file-permissions/
+ //
+ // User = read, write, execute
+ // Group = read, write, execute
+ // Other = read, write, execute
+ using (IProcessProxy process = processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
+ {
+ await process.StartAndWaitAsync(cancellationToken);
+ process.ThrowIfErrored();
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new UnauthorizedAccessException(
+ $"Access permissions denied for certificate directory '{certificateDirectory}'. Execute the installer with " +
+ $"sudo/root privileges to install SDK certificates in privileged locations.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
new file mode 100644
index 0000000000..c1a11cfe5b
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Dependencies
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.IO;
+ using System.IO.Abstractions;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Azure.Core;
+ using Azure.Identity;
+ using Microsoft.Extensions.DependencyInjection;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Virtual Client component that acquires an access token for an Azure Key Vault
+ /// using interactive browser or device-code authentication.
+ ///
+ public class KeyVaultAccessToken : VirtualClientComponent
+ {
+ private IFileSystem fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides all of the required dependencies to the Virtual Client component.
+ /// Parameters to the Virtual Client component.
+ public KeyVaultAccessToken(IServiceCollection dependencies, IDictionary parameters = null)
+ : base(dependencies, parameters)
+ {
+ this.fileSystem = dependencies.GetService();
+ this.fileSystem.ThrowIfNull(nameof(this.fileSystem));
+ }
+
+ ///
+ /// The Azure tenant ID used when requesting an access token for the Key Vault.
+ ///
+ protected string TenantId
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.TenantId));
+ }
+ }
+
+ ///
+ /// The Azure Key Vault URI for which an access token will be requested.
+ /// Example: https://anyvault.vault.azure.net/
+ ///
+ protected string KeyVaultUri
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.KeyVaultUri));
+ }
+ }
+
+ ///
+ /// The full file path where the acquired access token will be written,
+ /// when configured via / .
+ ///
+ protected string AccessTokenPath { get; set; }
+
+ ///
+ /// Initializes the component for execution, including resolving the access token
+ /// output path and removing any existing token file if configured.
+ ///
+ protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(this.LogFileName))
+ {
+ string directory = !string.IsNullOrWhiteSpace(this.LogFolderName)
+ ? this.LogFolderName
+ : this.fileSystem.Directory.GetCurrentDirectory();
+
+ this.AccessTokenPath = this.fileSystem.Path.GetFullPath(
+ this.fileSystem.Path.Combine(directory, this.LogFileName));
+
+ if (this.fileSystem.File.Exists(this.AccessTokenPath))
+ {
+ await this.fileSystem.File.DeleteAsync(this.AccessTokenPath);
+ }
+ }
+ }
+
+ ///
+ /// Acquires an access token for the configured Key Vault URI using Azure Identity.
+ /// Attempts interactive browser authentication first and falls back to
+ /// device-code authentication when a browser is not available.
+ /// The access token can optionally be written to a file and is always
+ /// written to the console output.
+ ///
+ protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.KeyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
+ this.TenantId.ThrowIfNullOrWhiteSpace(nameof(this.TenantId));
+
+ string accessToken = null;
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ string[] installerTenantResourceScopes = new string[]
+ {
+ new Uri(baseUri: new Uri(this.KeyVaultUri), relativeUri: ".default").ToString(),
+ // Example of a specific scope:
+ // "api://56e7ee83-1cf6-4048-a664-c2a08955f825/user_impersonation"
+ };
+
+ TokenRequestContext requestContext = new TokenRequestContext(scopes: installerTenantResourceScopes);
+
+ try
+ {
+ // Attempt an interactive (browser-based) authentication first. On most Windows environments
+ // this will work and is the most convenient for the user. On many Linux systems, there may
+ // not be a GUI and thus no browser. In that case, we fall back to the device code credential
+ // option in the catch block below.
+ InteractiveBrowserCredential credential = new InteractiveBrowserCredential(
+ new InteractiveBrowserCredentialOptions
+ {
+ TenantId = this.TenantId
+ });
+
+ AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
+ accessToken = response.Token;
+ }
+ catch (AuthenticationFailedException exc) when (exc.Message.Contains("Unable to open a web page"))
+ {
+ // Browser-based authentication is unavailable; switch to device code flow and present
+ // the user with a code and URL to complete authentication from another device.
+ DeviceCodeCredential credential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
+ {
+ TenantId = this.TenantId,
+ DeviceCodeCallback = (codeInfo, token) =>
+ {
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("Browser-based authentication unavailable (e.g. no GUI). Using device/code option.");
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("********************** Azure Key Vault Authorization **********************");
+ Console.WriteLine(string.Empty);
+ Console.WriteLine(codeInfo.Message);
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("***************************************************************************");
+ Console.WriteLine(string.Empty);
+
+ return Task.CompletedTask;
+ }
+ });
+
+ AccessToken token = await credential.GetTokenAsync(requestContext, cancellationToken);
+ accessToken = token.Token;
+ }
+
+ if (string.IsNullOrWhiteSpace(accessToken))
+ {
+ throw new AuthenticationFailedException("Authentication failed. No access token could be obtained.");
+ }
+
+ if (!string.IsNullOrEmpty(this.AccessTokenPath))
+ {
+ using (FileSystemStream fileStream = this.fileSystem.FileStream.New(
+ this.AccessTokenPath,
+ FileMode.Create,
+ FileAccess.ReadWrite,
+ FileShare.ReadWrite))
+ {
+ byte[] bytedata = Encoding.Default.GetBytes(accessToken);
+ fileStream.Write(bytedata, 0, bytedata.Length);
+ await fileStream.FlushAsync().ConfigureAwait(false);
+ this.Logger.LogTraceMessage($"Access token saved to file: {this.AccessTokenPath}");
+ }
+ }
+
+ Console.WriteLine("[Access Token]:");
+ Console.WriteLine(accessToken);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs
new file mode 100644
index 0000000000..71960099be
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO.Abstractions;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Contracts;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Contracts;
+ using System.IO;
+ using System.Runtime.ConstrainedExecution;
+ using System.Runtime.InteropServices;
+ using System.Security;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
+ using Azure;
+ using Azure.Security.KeyVault.Secrets;
+ using Polly;
+ using VirtualClient;
+ using VirtualClient.Identity;
+ using Serilog.Core;
+ using VirtualClient.Common.Telemetry;
+ using Azure.Core;
+ using Azure.Identity;
+
+ ///
+ /// Command resets the Virtual Client environment for "first time" run scenarios.
+ ///
+ public class CertificateInstallation : CommandBase
+ {
+ ///
+ /// Logger
+ ///
+ protected ILogger Logger { get; set; }
+
+ ///
+ /// Certificate Name.
+ ///
+ protected string CertificateName { get; set; }
+
+ ///
+ /// Executes the operations to reset the environment.
+ /// Installs certificates required by Virtual Client on the local system.
+ ///
+ /// The arguments provided to the application on the command line.
+ /// Provides a token that can be used to cancel the command operations.
+ /// The exit code for the command operations.
+ public override async Task ExecuteAsync(string[] args, CancellationTokenSource cancellationTokenSource)
+ {
+ CancellationToken cancellationToken = cancellationTokenSource.Token;
+ IServiceCollection dependencies = this.InitializeDependencies(args);
+ ISystemManagement systemManagement = dependencies.GetService();
+ ILogger logger = dependencies.GetService();
+
+ PlatformSpecifics platformSpecifics = dependencies.GetService();
+ PlatformID platform = platformSpecifics.Platform;
+
+ IKeyVaultManager keyVault = dependencies.GetService();
+
+ if (keyVault == null)
+ {
+ throw new DependencyException("Key Vault manager is not available.", ErrorReason.DependencyNotFound);
+ }
+
+ X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.CertificateName, cancellationToken);
+
+ try
+ {
+ Program.LogMessage(this.Logger, $"[Installing Certificates]", EventContext.Persisted());
+
+ if (platform == PlatformID.Unix)
+ {
+ await this.InstallCertificateOnUnixAsync(certificate, dependencies, cancellationToken);
+ }
+ else if (platform == PlatformID.Win32NT)
+ {
+ await this.InstallCertificateOnWindowsAsync(certificate);
+ }
+ else
+ {
+ throw new DependencyException(
+ $"Certificate installation for OS platform '{platform}' is not supported.",
+ ErrorReason.NotSupported);
+ }
+ }
+ catch (CryptographicException exc) when (exc.Message.Contains("access", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new DependencyException(
+ $"Certificate installation failed. Local certificate store access permissions denied. Virtual Client must be " +
+ $"running with Administrative privileges in order to install certificates in the current context.",
+ ErrorReason.DependencyInstallationFailed);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Unix/Linux system.
+ /// Handles both root and sudo scenarios.
+ ///
+ protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, IServiceCollection dependencies, CancellationToken cancellationToken)
+ {
+ PlatformSpecifics platformSpecifics = dependencies.GetService();
+ ProcessManager processManager = dependencies.GetService();
+ IFileSystem fileSystem = dependencies.GetService();
+
+ // On Unix/Linux systems, we install ther certificate in the default location for the
+ // user as well as in a static location. In the future we will likely use the static location
+ // only.
+ string certificateDirectory = null;
+
+ try
+ {
+ // When "sudo" is used to run the installer, we need to know the logged
+ // in user account. On Linux systems, there is an environment variable 'SUDO_USER'
+ // that defines the logged in user.
+ string user = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.USER);
+ string sudoUser = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.SUDO_USER);
+ certificateDirectory = $"/home/{user}/.dotnet/corefx/cryptography/x509stores/my";
+
+ if (!string.IsNullOrWhiteSpace(sudoUser))
+ {
+ // The installer is being executed with "sudo" privileges. We want to use the
+ // logged in user profile vs. "root".
+ certificateDirectory = $"/home/{sudoUser}/.dotnet/corefx/cryptography/x509stores/my";
+ }
+ else if (user == "root")
+ {
+ // The installer is being executed from the "root" account on Linux.
+ certificateDirectory = $"/root/.dotnet/corefx/cryptography/x509stores/my";
+ }
+
+ Program.LogMessage(this.Logger, $"Certificate Store = {certificateDirectory}", EventContext.Persisted());
+
+ if (!fileSystem.Directory.Exists(certificateDirectory))
+ {
+ fileSystem.Directory.CreateDirectory(certificateDirectory);
+ }
+
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+
+ await fileSystem.File.WriteAllBytesAsync(
+ platformSpecifics.Combine(certificateDirectory, $"{certificate.Thumbprint}.pfx"),
+ certificate.Export(X509ContentType.Pfx));
+
+ // Permissions 777 (-rwxrwxrwx)
+ // https://linuxhandbook.com/linux-file-permissions/
+ //
+ // User = read, write, execute
+ // Group = read, write, execute
+ // Other = read, write, execute
+ using (IProcessProxy process = processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
+ {
+ await process.StartAndWaitAsync(cancellationToken);
+ process.ThrowIfErrored();
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new UnauthorizedAccessException(
+ $"Access permissions denied for certificate directory '{certificateDirectory}'. Run Virtual Client with " +
+ $"sudo/root privileges to install certificates in privileged locations.");
+ }
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Windows system.
+ ///
+ protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certificate)
+ {
+ return Task.Run(() =>
+ {
+ Program.LogMessage(this.Logger, $"Certificate Store = CurrentUser/Personal", EventContext.Persisted());
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs b/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs
new file mode 100644
index 0000000000..8c54d2965e
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using Azure.Storage.Blobs;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos;
+ using Newtonsoft.Json;
+ using Polly;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Globalization;
+ using System.IO;
+ using System.IO.Abstractions;
+ using System.Linq;
+ using System.Net;
+ using System.Runtime.InteropServices;
+ using System.Security.Cryptography.X509Certificates;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Contracts;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+ using VirtualClient.Contracts.Metadata;
+ using VirtualClient.Contracts.Validation;
+ using VirtualClient.Logging;
+ using VirtualClient.Metadata;
+
+ ///
+ /// Command that executes a profile to acquire an access token for an Azure Key Vault.
+ ///
+ internal class GetAccessTokenCommand : ExecuteProfileCommand
+ {
+ ///
+ /// Executes the access token acquisition operations using the configured profile.
+ ///
+ /// The arguments provided to the application on the command line.
+ /// Provides a token that can be used to cancel the command operations.
+ /// The exit code for the command operations.
+ public override Task ExecuteAsync(string[] args, CancellationTokenSource cancellationTokenSource)
+ {
+ this.Timeout = ProfileTiming.OneIteration();
+ this.Profiles = new List
+ {
+ new DependencyProfileReference("GET-ACCESS-TOKEN.json")
+ };
+
+ IServiceCollection dependencies = this.InitializeDependencies(args);
+ this.Parameters = this.ResolveParameters();
+
+ return base.ExecuteAsync(args, cancellationTokenSource);
+ }
+
+ private Dictionary ResolveParameters()
+ {
+ ////IKeyVaultManager keyVaultManager = dependencies.GetService();
+ ////var a = keyVaultManager.StoreDescription;
+
+ var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if(string.IsNullOrWhiteSpace(this.KeyVault))
+ {
+ Uri baseUri = new Uri(this.KeyVault);
+ var store = new DependencyKeyVaultStore("KeyVault", baseUri);
+ }
+
+ return parameters;
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
index 7ddc9999ef..c9688b3804 100644
--- a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
+++ b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
@@ -598,6 +598,48 @@ public static Option CreateKeyVaultOption(bool required = false, object defaultV
return option;
}
+ ///
+ /// Command line option defines a certificate name to download from Azure keyvault.
+ ///
+ /// Sets this option as required.
+ /// Sets the default value when none is provided.
+ public static Option CreateCertNameOption(bool required = false, object defaultValue = null)
+ {
+ Option option = new Option(
+ new string[] { "--certName", "--cert-name", "--cert-Name" })
+ {
+ Name = "CertificateName",
+ Description = "An endpoint URI or connection string to the Key Vault from which secrets and certificates can be accessed.",
+ ArgumentHelpName = "CertificatesName",
+ AllowMultipleArgumentsPerToken = false
+ };
+
+ OptionFactory.SetOptionRequirements(option, required, defaultValue);
+
+ return option;
+ }
+
+ ///
+ /// Command line option defines an access token to authenticate with Azure key vault.
+ ///
+ /// Sets this option as required.
+ /// Sets the default value when none is provided.
+ public static Option CreateAccessTokenOption(bool required = false, object defaultValue = null)
+ {
+ Option option = new Option(
+ new string[] { "--token", "--access-token" })
+ {
+ Name = "AccessToken",
+ Description = "An access token to authenticate with Azure key vault.",
+ ArgumentHelpName = "Access Token",
+ AllowMultipleArgumentsPerToken = false
+ };
+
+ OptionFactory.SetOptionRequirements(option, required, defaultValue);
+
+ return option;
+ }
+
///
/// Command line option defines the path to the environment layout file.
///
diff --git a/src/VirtualClient/VirtualClient.Main/Program.cs b/src/VirtualClient/VirtualClient.Main/Program.cs
index db50e303ed..309a5f68f3 100644
--- a/src/VirtualClient/VirtualClient.Main/Program.cs
+++ b/src/VirtualClient/VirtualClient.Main/Program.cs
@@ -330,6 +330,11 @@ internal static CommandLineBuilder SetupCommandLine(string[] args, CancellationT
bootstrapSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
rootCommand.Add(bootstrapSubcommand);
+ Command getAccessTokenSubcommand = Program.CreateGetTokenSubCommand(settings);
+ getAccessTokenSubcommand.TreatUnmatchedTokensAsErrors = true;
+ getAccessTokenSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
+ rootCommand.Add(getAccessTokenSubcommand);
+
Command cleanSubcommand = Program.CreateCleanSubcommand(settings);
cleanSubcommand.TreatUnmatchedTokensAsErrors = true;
cleanSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
@@ -406,6 +411,23 @@ private static Command CreateApiSubcommand(DefaultSettings settings)
return apiCommand;
}
+ private static Command CreateGetTokenSubCommand(DefaultSettings settings)
+ {
+ Command getAccessTokenCommand = new Command(
+ "get-token",
+ "Get access token for current user to authenticate with Azure Key Vault.")
+ {
+ // OPTIONAL
+ // -------------------------------------------------------------------
+ OptionFactory.CreateParametersOption(required: false),
+
+ // --key-vault
+ OptionFactory.CreateKeyVaultOption(required: false)
+ };
+
+ return getAccessTokenCommand;
+ }
+
private static Command CreateBootstrapSubcommand(DefaultSettings settings)
{
Command bootstrapCommand = new Command(
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json b/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json
new file mode 100644
index 0000000000..3e6d6f2c4e
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json
@@ -0,0 +1,19 @@
+{
+ "Description": "Get access token for the user that can be used to authenticate.",
+ "Parameters": {
+ "KeyVaultUri": null,
+ "TenantId": null,
+ "LogFileName": "AccessToken.txt"
+ },
+ "Dependencies": [
+ {
+ "Type": "KeyVaultAccessToken",
+ "Parameters": {
+ "Scenario": "GetKVAccessToken",
+ "TenantId": "$.Parameters.TenantId",
+ "KeyVaultUri": "$.Parameters.KeyVaultUri",
+ "LogFileName": "$.Parameters.LogFileName"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
new file mode 100644
index 0000000000..95cf3519bd
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
@@ -0,0 +1,23 @@
+{
+ "Description": "Installs certificate from a Azure Key Vault.",
+ "Parameters": {
+ "AccessToken": "{access token}",
+ "KeyVaultUri": "https://yourkeyvault.vault.azure.net/",
+ "CertificateName": "cert-01-name",
+ "CertificatePassword": "",
+ "TenantId": ""
+ },
+ "Dependencies": [
+ {
+ "Type": "CertificateInstallation",
+ "Parameters": {
+ "Scenario": "InstallCertificate",
+ "AccessToken": "$.Parameters.AccessToken",
+ "TenantId": "$.Parameters.TenantId",
+ "KeyVaultUri": "$.Parameters.KeyVaultUri",
+ "CertificateName": "$.Parameters.CertificateName",
+ "CertificatePassword": "$.Parameters.CertificatePassword"
+ }
+ }
+ ]
+}
\ No newline at end of file