Skip to content

Commit 18ccb60

Browse files
Nirjan Chapagainnchapagain001
authored andcommitted
Allowing cert to be exported even after loading.
1 parent 032fb2b commit 18ccb60

3 files changed

Lines changed: 91 additions & 28 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
namespace VirtualClient.Identity
4+
{
5+
using System.Security.Cryptography.X509Certificates;
6+
7+
/// <summary>
8+
/// Certificate Loader to cleanly handle differences in .NET versions for loading certificates from byte arrays.
9+
/// </summary>
10+
internal static class CertificateLoaderHelper
11+
{
12+
internal static X509Certificate2 LoadPublic(byte[] cerBytes)
13+
{
14+
#if NET9_0_OR_GREATER
15+
return X509CertificateLoader.LoadCertificate(cerBytes);
16+
#else
17+
return new X509Certificate2(cerBytes);
18+
#endif
19+
}
20+
21+
internal static X509Certificate2 LoadPkcs12(
22+
byte[] pfxBytes,
23+
string password,
24+
X509KeyStorageFlags flags)
25+
{
26+
#if NET9_0_OR_GREATER
27+
return X509CertificateLoader.LoadPkcs12(
28+
pfxBytes,
29+
password,
30+
flags);
31+
#else
32+
return new X509Certificate2(
33+
pfxBytes,
34+
password,
35+
flags);
36+
#endif
37+
}
38+
}
39+
}

src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace VirtualClient
1515
using Azure.Security.KeyVault.Secrets;
1616
using Polly;
1717
using VirtualClient.Common.Extensions;
18+
using VirtualClient.Identity;
1819

1920
/// <summary>
2021
/// Provides methods for retrieving secrets, keys, and certificates from an Azure Key Vault.
@@ -211,40 +212,50 @@ public async Task<X509Certificate2> GetCertificateAsync(
211212
this.StoreDescription.ThrowIfNull(nameof(this.StoreDescription));
212213
certName.ThrowIfNullOrWhiteSpace(nameof(certName), "The certificate name cannot be null or empty.");
213214

214-
// Use the keyVaultUri if provided as a parameter, otherwise use the store's EndpointUri
215215
Uri vaultUri = !string.IsNullOrWhiteSpace(keyVaultUri)
216216
? new Uri(keyVaultUri)
217217
: ((DependencyKeyVaultStore)this.StoreDescription).EndpointUri;
218218

219219
CertificateClient client = this.CreateCertificateClient(vaultUri, ((DependencyKeyVaultStore)this.StoreDescription).Credentials);
220220

221+
var credentials = ((DependencyKeyVaultStore)this.StoreDescription).Credentials;
222+
223+
CertificateClient certificateClient = this.CreateCertificateClient(vaultUri, credentials);
224+
SecretClient secretClient = this.CreateSecretClient(vaultUri, credentials);
225+
221226
try
222227
{
223228
return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
224229
{
225230
// Get the full certificate with private key (PFX) if requested
226231
if (retrieveWithPrivateKey)
227232
{
228-
X509Certificate2 privateKeyCert = await client
229-
.DownloadCertificateAsync(certName, cancellationToken: cancellationToken)
230-
.ConfigureAwait(false);
233+
KeyVaultSecret secret = await secretClient.GetSecretAsync(certName, cancellationToken: cancellationToken);
234+
235+
if (secret?.Value == null)
236+
{
237+
throw new DependencyException($"Secret for certificate '{certName}' not found in vault '{vaultUri}'.");
238+
}
239+
240+
byte[] pfxBytes = Convert.FromBase64String(secret.Value);
231241

232-
if (privateKeyCert is null || !privateKeyCert.HasPrivateKey)
242+
X509Certificate2 pfxCertificate = CertificateLoaderHelper.LoadPkcs12(
243+
pfxBytes,
244+
string.Empty,
245+
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
246+
247+
if (!pfxCertificate.HasPrivateKey)
233248
{
234-
throw new DependencyException("Failed to retrieve certificate content with private key.");
249+
throw new DependencyException($"Certificate '{certName}' does not contain a private key.");
235250
}
236251

237-
return privateKeyCert;
252+
return pfxCertificate;
238253
}
239254
else
240255
{
241-
// If private key not needed, load cert from PublicBytes
242-
KeyVaultCertificateWithPolicy cert = await client.GetCertificateAsync(certName, cancellationToken: cancellationToken);
243-
#if NET9_0_OR_GREATER
244-
return X509CertificateLoader.LoadCertificate(cert.Cer);
245-
#elif NET8_0_OR_GREATER
246-
return new X509Certificate2(cert.Cer);
247-
#endif
256+
// Public certificate only
257+
KeyVaultCertificateWithPolicy certBundle = await certificateClient.GetCertificateAsync(certName, cancellationToken: cancellationToken);
258+
return CertificateLoaderHelper.LoadPublic(certBundle.Cer);
248259
}
249260
}).ConfigureAwait(false);
250261
}
@@ -269,13 +280,6 @@ public async Task<X509Certificate2> GetCertificateAsync(
269280
ex,
270281
ErrorReason.HttpNonSuccessResponse);
271282
}
272-
catch (Exception ex)
273-
{
274-
throw new DependencyException(
275-
$"Failed to get certificate '{certName}' from vault '{vaultUri}'.",
276-
ex,
277-
ErrorReason.HttpNonSuccessResponse);
278-
}
279283
}
280284

281285
/// <summary>
@@ -328,4 +332,4 @@ private void ValidateKeyVaultStore()
328332
}
329333
}
330334
}
331-
}
335+
}

src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string CertificateName
6262
/// <summary>
6363
/// Gets the path to the file where the access token is saved.
6464
/// </summary>
65-
public string AccessTokenPath
65+
public string AccessTokenPath
6666
{
6767
get
6868
{
@@ -138,12 +138,16 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
138138
throw new PlatformNotSupportedException($"The '{nameof(CertificateInstallation)}' component is not supported on platform '{this.Platform}'.");
139139
}
140140

141-
// If a download directory is specified, we will also export the certificate to that location.
141+
// Export the certificate if requested
142142
if (!string.IsNullOrEmpty(this.CertificateDownloadDir))
143143
{
144-
string certificateFileName = this.WithPrivateKey
144+
string certificateFileName = this.WithPrivateKey
145145
? $"{this.CertificateName}.pfx"
146146
: $"{this.CertificateName}.cer";
147+
148+
X509ContentType contentType = this.WithPrivateKey
149+
? X509ContentType.Pfx
150+
: X509ContentType.Cert;
147151

148152
string certificatePath = this.Combine(this.CertificateDownloadDir, certificateFileName);
149153

@@ -153,9 +157,8 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
153157
this.fileSystem.File.Delete(certificatePath);
154158
}
155159

156-
// Export the new certificate
157-
await this.fileSystem.File.WriteAllBytesAsync(certificatePath, certificate.Export(X509ContentType.Pfx));
158-
Console.WriteLine($"Certificate exported to {certificatePath}");
160+
byte[] certBytes = certificate.Export(contentType, string.Empty);
161+
await this.fileSystem.File.WriteAllBytesAsync(certificatePath, certBytes);
159162
}
160163
}
161164
catch (Exception exc)
@@ -276,5 +279,22 @@ protected IKeyVaultManager GetKeyVaultManager()
276279
$"Either valid --KeyVault or --Token or --TokenPath must be passed in order to set up authentication with Key Vault.");
277280
}
278281
}
282+
283+
/// <summary>
284+
/// Tries to get certificate data, returning null if an exception occurs.
285+
/// </summary>
286+
private byte[] TryGetCertData(Func<byte[]> getCertData)
287+
{
288+
try
289+
{
290+
return getCertData();
291+
}
292+
catch (Exception exc)
293+
{
294+
Console.WriteLine(exc.ToString());
295+
Console.WriteLine("\n\n\n\n=================================================================================\n");
296+
return null;
297+
}
298+
}
279299
}
280300
}

0 commit comments

Comments
 (0)