Skip to content

Commit cb9d335

Browse files
Nirjan Chapagainnchapagain001
authored andcommitted
Addressing PR comments
1 parent 51a4e62 commit cb9d335

9 files changed

Lines changed: 230 additions & 588 deletions

File tree

src/VirtualClient/VirtualClient.Dependencies.UnitTests/CertificateInstallationTests.cs

Lines changed: 155 additions & 457 deletions
Large diffs are not rendered by default.

src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs

Lines changed: 53 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
/// Virtual Client component that installs certificates from Azure Key Vault
1818
/// into the appropriate certificate store for the operating system.
1919
/// </summary>
20+
[SupportedPlatforms("linux-arm64,linux-x64,win-arm64,win-x64", true)]
2021
public class CertificateInstallation : VirtualClientComponent
2122
{
2223
private ISystemManagement systemManagement;
@@ -84,11 +85,11 @@ public bool WithPrivateKey
8485
/// <summary>
8586
/// Gets the directory where the certificate will be exported. If not provided, the certificate will not be exported to a file.
8687
/// </summary>
87-
public string CertificateDownloadDir
88+
public string CertificateInstallationDir
8889
{
8990
get
9091
{
91-
return this.Parameters.GetValue<string>(nameof(this.CertificateDownloadDir), string.Empty);
92+
return this.Parameters.GetValue<string>(nameof(this.CertificateInstallationDir));
9293
}
9394
}
9495

@@ -125,39 +126,11 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
125126
IKeyVaultManager keyVault = this.GetKeyVaultManager();
126127
X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.CertificateName, cancellationToken, null, this.WithPrivateKey);
127128

128-
if (this.Platform == PlatformID.Win32NT)
129-
{
130-
await this.InstallCertificateOnWindowsAsync(certificate, cancellationToken);
131-
}
132-
else if (this.Platform == PlatformID.Unix)
133-
{
134-
await this.InstallCertificateOnUnixAsync(certificate, cancellationToken);
135-
}
136-
else
137-
{
138-
throw new PlatformNotSupportedException($"The '{nameof(CertificateInstallation)}' component is not supported on platform '{this.Platform}'.");
139-
}
129+
await this.InstallCertificateOnMachineAsync(certificate, cancellationToken);
140130

141-
// Export the certificate if requested
142-
if (!string.IsNullOrEmpty(this.CertificateDownloadDir))
131+
if (!string.IsNullOrWhiteSpace(this.CertificateInstallationDir))
143132
{
144-
string certificateFileName = this.WithPrivateKey
145-
? $"{this.CertificateName}.pfx"
146-
: $"{this.CertificateName}.cer";
147-
148-
X509ContentType contentType = this.WithPrivateKey
149-
? X509ContentType.Pfx
150-
: X509ContentType.Cert;
151-
152-
string certificatePath = this.Combine(this.CertificateDownloadDir, certificateFileName);
153-
154-
if (this.fileSystem.File.Exists(certificatePath))
155-
{
156-
this.fileSystem.File.Delete(certificatePath);
157-
}
158-
159-
byte[] certBytes = certificate.Export(contentType, string.Empty);
160-
await this.fileSystem.File.WriteAllBytesAsync(certificatePath, certBytes);
133+
await this.InstallCertificateLocallyAsync(certificate, cancellationToken);
161134
}
162135
}
163136
catch (Exception exc)
@@ -169,9 +142,9 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
169142
}
170143

171144
/// <summary>
172-
/// Installs the certificate in the appropriate certificate store on a Windows system.
145+
/// Installs the certificate in the appropriate certificate store.
173146
/// </summary>
174-
protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
147+
protected virtual Task InstallCertificateOnMachineAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
175148
{
176149
return Task.Run(() =>
177150
{
@@ -185,72 +158,6 @@ protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certifi
185158
});
186159
}
187160

188-
/// <summary>
189-
/// Installs the certificate in the appropriate certificate store on a Unix/Linux system.
190-
/// </summary>
191-
protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
192-
{
193-
// On Unix/Linux systems, we install the certificate in the default location for the
194-
// user as well as in a static location. In the future we will likely use the static location
195-
// only.
196-
string certificateDirectory = null;
197-
198-
try
199-
{
200-
// When "sudo" is used to run the installer, we need to know the logged
201-
// in user account. On Linux systems, there is an environment variable 'SUDO_USER'
202-
// that defines the logged in user.
203-
204-
string user = this.GetEnvironmentVariable(EnvironmentVariable.USER);
205-
string sudoUser = this.GetEnvironmentVariable(EnvironmentVariable.SUDO_USER);
206-
certificateDirectory = $"/home/{user}/.dotnet/corefx/cryptography/x509stores/my";
207-
208-
if (!string.IsNullOrWhiteSpace(sudoUser))
209-
{
210-
// The installer is being executed with "sudo" privileges. We want to use the
211-
// logged in user profile vs. "root".
212-
certificateDirectory = $"/home/{sudoUser}/.dotnet/corefx/cryptography/x509stores/my";
213-
}
214-
else if (user == "root")
215-
{
216-
// The installer is being executed from the "root" account on Linux.
217-
certificateDirectory = $"/root/.dotnet/corefx/cryptography/x509stores/my";
218-
}
219-
220-
Console.WriteLine($"Certificate Store = {certificateDirectory}");
221-
222-
if (!this.fileSystem.Directory.Exists(certificateDirectory))
223-
{
224-
this.fileSystem.Directory.CreateDirectory(certificateDirectory);
225-
}
226-
227-
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
228-
{
229-
store.Open(OpenFlags.ReadWrite);
230-
store.Add(certificate);
231-
store.Close();
232-
}
233-
234-
await this.fileSystem.File.WriteAllBytesAsync(
235-
this.Combine(certificateDirectory, $"{certificate.Thumbprint}.pfx"),
236-
certificate.Export(X509ContentType.Pfx));
237-
238-
// Permissions 777 (-rwxrwxrwx)
239-
// https://linuxhandbook.com/linux-file-permissions/
240-
using (IProcessProxy process = this.processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
241-
{
242-
await process.StartAndWaitAsync(cancellationToken);
243-
process.ThrowIfErrored<DependencyException>();
244-
}
245-
}
246-
catch (UnauthorizedAccessException)
247-
{
248-
throw new UnauthorizedAccessException(
249-
$"Access permissions denied for certificate directory '{certificateDirectory}'. Execute the installer with " +
250-
$"sudo/root privileges to install SDK certificates in privileged locations.");
251-
}
252-
}
253-
254161
/// <summary>
255162
/// Gets the Key Vault manager to use to retrieve certificates from Key Vault.
256163
/// </summary>
@@ -278,5 +185,50 @@ protected IKeyVaultManager GetKeyVaultManager()
278185
$"Either valid --KeyVault or --Token or --TokenPath must be passed in order to set up authentication with Key Vault.");
279186
}
280187
}
188+
189+
/// <summary>
190+
/// Installs the certificate in static location
191+
/// </summary>
192+
protected async Task InstallCertificateLocallyAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
193+
{
194+
try
195+
{
196+
string certificateFileName = this.WithPrivateKey
197+
? $"{this.CertificateName}.pfx"
198+
: $"{this.CertificateName}.cer";
199+
200+
X509ContentType contentType = this.WithPrivateKey
201+
? X509ContentType.Pfx
202+
: X509ContentType.Cert;
203+
204+
byte[] certBytes = certificate.Export(contentType, string.Empty);
205+
206+
string certificatePath = this.Combine(this.CertificateInstallationDir, certificateFileName);
207+
208+
if (!this.fileSystem.Directory.Exists(this.CertificateInstallationDir))
209+
{
210+
this.fileSystem.Directory.CreateDirectory(this.CertificateInstallationDir);
211+
}
212+
213+
await this.fileSystem.File.WriteAllBytesAsync(certificatePath, certBytes);
214+
215+
if (this.Platform == PlatformID.Unix)
216+
{
217+
// Permissions 777 (-rwxrwxrwx)
218+
// https://linuxhandbook.com/linux-file-permissions/
219+
using (IProcessProxy process = this.processManager.CreateProcess("chmod", $"-R 777 {this.CertificateInstallationDir}"))
220+
{
221+
await process.StartAndWaitAsync(cancellationToken);
222+
process.ThrowIfErrored<DependencyException>();
223+
}
224+
}
225+
}
226+
catch (UnauthorizedAccessException)
227+
{
228+
throw new UnauthorizedAccessException(
229+
$"Access permissions denied for certificate directory '{this.CertificateInstallationDir}'. Execute the installer with " +
230+
$"admin/sudo/root privileges to install certificates in privileged locations.");
231+
}
232+
}
281233
}
282234
}

src/VirtualClient/VirtualClient.Main/BootstrapCommand.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,9 @@ internal class BootstrapCommand : ExecuteProfileCommand
5454
public string TenantId { get; set; }
5555

5656
/// <summary>
57-
/// Directory path where downloaded certificates can be stored.
57+
/// Directory path where certificates can be stored. This is optional, but if not provided, certificates will not be saved to disk.
5858
/// </summary>
59-
/// <remarks>Set this property to a valid directory path to ensure that certificates can be
60-
/// downloaded and saved.
61-
/// </remarks>
62-
public string CertificateDownloadDir { get; set; }
59+
public string CertificateInstallationDir { get; set; }
6360

6461
/// <summary>
6562
/// Executes the bootstrap command.
@@ -147,9 +144,9 @@ protected void SetupCertificateInstallation()
147144
"The Key Vault URI must be provided (--key-vault) when installing certificates (--cert-name).");
148145
}
149146

150-
if (!string.IsNullOrWhiteSpace(this.CertificateDownloadDir))
147+
if (!string.IsNullOrWhiteSpace(this.CertificateInstallationDir))
151148
{
152-
this.Parameters["CertificateDownloadDir"] = this.CertificateDownloadDir;
149+
this.Parameters["CertificateInstallationDir"] = this.CertificateInstallationDir;
153150
}
154151

155152
// Set certificate-related parameters

src/VirtualClient/VirtualClient.Main/OptionFactory.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ public static Option CreateKeyVaultOption(bool required = false, object defaultV
605605
/// <param name="defaultValue">Sets the default value when none is provided.</param>
606606
public static Option CreateTokenOption(bool required = false, object defaultValue = null)
607607
{
608-
Option<string> option = new Option<string>(new string[] { "--token", "--access-token" })
608+
Option<string> option = new Option<string>(new string[] { "--token" })
609609
{
610610
Name = "AccessToken",
611611
Description = "Authentication token for Azure Key Vault access. When not provided, uses default Azure credential authentication (Azure CLI, Managed Identity, etc.).",
@@ -624,7 +624,7 @@ public static Option CreateTokenOption(bool required = false, object defaultValu
624624
/// <param name="defaultValue">Sets the default value when none is provided.</param>
625625
public static Option CreateCertificateNameOption(bool required = false, object defaultValue = null)
626626
{
627-
Option<string> option = new Option<string>(new string[] { "--certname", "--certificate-name", "--cert-name" })
627+
Option<string> option = new Option<string>(new string[] {"--certificate-name", "--cert-name" })
628628
{
629629
Name = "CertificateName",
630630
Description = "The name of the certificate in Azure Key Vault to install to the local certificate store.",
@@ -643,7 +643,7 @@ public static Option CreateCertificateNameOption(bool required = false, object d
643643
/// <param name="defaultValue">Sets the default value when none is provided.</param>
644644
public static Option CreateTenantIdOption(bool required = false, object defaultValue = null)
645645
{
646-
Option<string> option = new Option<string>(new string[] { "--tenant-id", "--tid" })
646+
Option<string> option = new Option<string>(new string[] { "--tenant-id" })
647647
{
648648
Name = "TenantId",
649649
Description = "The tenant ID associated with your Microsoft Entra ID.",
@@ -660,11 +660,11 @@ public static Option CreateTenantIdOption(bool required = false, object defaultV
660660
/// </summary>
661661
/// <param name="required">Sets this option as required.</param>
662662
/// <param name="defaultValue">Sets the default value when none is provided.</param>
663-
public static Option CreateCertificateDownloadDirectoryOption(bool required = false, object defaultValue = null)
663+
public static Option CreateCertificateInstallationDirectoryOption(bool required = false, object defaultValue = null)
664664
{
665-
Option<string> option = new Option<string>(new string[] { "--certificateDownloadDir", "--certDownloadDir" })
665+
Option<string> option = new Option<string>(new string[] { "--cert-installation-dir" })
666666
{
667-
Name = "CertificateDownloadDir",
667+
Name = "CertificateInstallationDir",
668668
Description = "Defines the directory where certificates downloaded from Key Vault will be saved.",
669669
ArgumentHelpName = "path",
670670
AllowMultipleArgumentsPerToken = false

src/VirtualClient/VirtualClient.Main/Program.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,7 @@ internal static CommandLineBuilder SetupCommandLine(string[] args, CancellationT
311311

312312
// --verbose
313313
OptionFactory.CreateVerboseFlag(required: false, false),
314-
315-
// --token
316-
OptionFactory.CreateTokenOption(required: false),
317-
318-
// --CertificateDownloadDir
319-
OptionFactory.CreateCertificateDownloadDirectoryOption(required: false),
320-
314+
321315
// --tenant-id
322316
OptionFactory.CreateTenantIdOption(required: false),
323317
};
@@ -481,8 +475,8 @@ private static Command CreateBootstrapSubcommand(DefaultSettings settings)
481475
// --token
482476
OptionFactory.CreateTokenOption(required: false),
483477

484-
// --CertificateDownloadDir
485-
OptionFactory.CreateCertificateDownloadDirectoryOption(required: false),
478+
// --cert-installation-dir
479+
OptionFactory.CreateCertificateInstallationDirectoryOption(required: false),
486480

487481
// --tenant-id
488482
OptionFactory.CreateTenantIdOption(required: false),

src/VirtualClient/VirtualClient.Main/profiles/BOOTSTRAP-CERTIFICATE.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"CertificateName": null,
66
"AccessToken": null,
77
"LogFileName": null,
8-
"CertificateDownloadDir": null
8+
"CertificateInstallationDir": null,
9+
"WithPrivateKey": true
910
},
1011
"Actions": [
1112
{
@@ -16,7 +17,8 @@
1617
"CertificateName": "$.Parameters.CertificateName",
1718
"AccessToken": "$.Parameters.AccessToken",
1819
"AccessTokenPath": "$.Parameters.LogFileName",
19-
"CertificateDownloadDir": "$.Parameters.CertificateDownloadDir"
20+
"CertificateInstallationDir": "$.Parameters.CertificateInstallationDir",
21+
"WithPrivateKey": "$.Parameters.WithPrivateKey"
2022
}
2123
}
2224
]

src/VirtualClient/VirtualClient.UnitTests/BootstrapCommandTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,14 @@ public void SetupCertificateInstallation_AllowsCertificateDownloadDirectory()
137137
KeyVault = "https://myvault.vault.azure.net/",
138138
AccessToken = "token123",
139139
TenantId = "00000000-0000-0000-0000-000000000001",
140-
CertificateDownloadDir = "C:\\certs"
140+
CertificateInstallationDir = "C:\\certs"
141141
};
142142

143143
command.SetupCertificateInstallationPublic();
144144

145145
Assert.AreEqual(command.Parameters["KeyVaultUri"], command.KeyVault);
146146
Assert.AreEqual(command.Parameters["CertificateName"], command.CertificateName);
147-
Assert.AreEqual(command.Parameters["CertificateDownloadDir"], command.CertificateDownloadDir);
147+
Assert.AreEqual(command.Parameters["CertificateInstallationDir"], command.CertificateInstallationDir);
148148
Assert.AreEqual(command.Parameters.Count, 3);
149149
}
150150

src/VirtualClient/VirtualClient.UnitTests/BootstrapCommandValidatorTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public void BootstrapCommand_RequiresAtLeastOneOperation()
2727

2828
[Test]
2929
[TestCase("--cert-name")]
30-
[TestCase("--certname")]
3130
[TestCase("--certificate-name")]
3231
public void BootstrapCommand_CertificateInstall_RequiresKeyVault(string certAlias)
3332
{

0 commit comments

Comments
 (0)