Skip to content

Commit dee0fce

Browse files
author
Aditya Abhishek
committed
resolve comments
1 parent 155e7c4 commit dee0fce

3 files changed

Lines changed: 102 additions & 59 deletions

File tree

src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using Polly;
1212
using System;
1313
using System.Net;
14+
using System.Security.Cryptography;
15+
using System.Security.Cryptography.X509Certificates;
1416
using System.Threading;
1517
using System.Threading.Tasks;
1618
using VirtualClient.Contracts;
@@ -58,18 +60,24 @@ public void SetupDefaultBehaviors()
5860
.ReturnsAsync(Response.FromValue(key, Mock.Of<Response>()));
5961

6062
// Mock the certificates
63+
6164
this.certificateClientMock = new Mock<CertificateClient>(MockBehavior.Strict, new Uri("https://myvault.vault.azure.net/"), new MockTokenCredential());
62-
var certificate = CertificateModelFactory.KeyVaultCertificateWithPolicy(
65+
var publicKeyCertificate = CertificateModelFactory.KeyVaultCertificateWithPolicy(
6366
properties: CertificateModelFactory.CertificateProperties(
6467
id: new Uri($"https://myvault.vault.azure.net/certificates/mycert/v3"),
6568
name: "mycert",
6669
version: "v3",
6770
vaultUri: new Uri("https://myvault.vault.azure.net/")),
68-
policy: CertificateModelFactory.CertificatePolicy(subject: "CN=mycert"));
71+
policy: CertificateModelFactory.CertificatePolicy(subject: "CN=mycert"),
72+
cer: this.GenerateTestCertificateBytes());
6973

7074
this.certificateClientMock
7175
.Setup(c => c.GetCertificateAsync("mycert", It.IsAny<CancellationToken>()))
72-
.ReturnsAsync(Response.FromValue(certificate, Mock.Of<Response>()));
76+
.ReturnsAsync(Response.FromValue(publicKeyCertificate, Mock.Of<Response>()));
77+
78+
this.certificateClientMock
79+
.Setup(c => c.DownloadCertificateAsync("mycert", It.IsAny<string>(), It.IsAny<CancellationToken>()))
80+
.ReturnsAsync(Response.FromValue(this.GenerateTestCertificateWithPrivateKey(), Mock.Of<Response>()));
7381

7482
// Initialize the KeyVaultManager with the mocked clients
7583
this.keyVaultManager = new TestKeyVaultManager(
@@ -89,36 +97,38 @@ public void KeyVaultManagerConstructorsValidateRequiredParameters()
8997
}
9098

9199
[Test]
92-
public async Task KeyVaultManagerReturnsExpectedSecretDescriptor()
100+
public async Task KeyVaultManagerReturnsExpectedSecretValue()
93101
{
94102
var result = await this.keyVaultManager.GetSecretAsync(this.mockDescriptor, CancellationToken.None, Policy.NoOpAsync());
95103
Assert.IsNotNull(result);
96-
Assert.AreEqual("mysecret", result.Name);
97-
Assert.AreEqual("secret-value", result.Value);
98-
Assert.AreEqual("v1", result.Version);
99-
Assert.AreEqual(KeyVaultObjectType.Secret, result.ObjectType);
104+
Assert.AreEqual("secret-value", result);
100105
}
101106

102107
[Test]
103-
public async Task KeyVaultManagerReturnsExpectedKeyDescriptor()
108+
public async Task KeyVaultManagerReturnsExpectedKey()
104109
{
105110
this.mockDescriptor.Name = "mykey";
106111
var result = await this.keyVaultManager.GetKeyAsync(this.mockDescriptor, CancellationToken.None, Policy.NoOpAsync());
107112
Assert.IsNotNull(result);
108113
Assert.AreEqual("mykey", result.Name);
109-
Assert.AreEqual("v2", result.Version);
110-
Assert.AreEqual(KeyVaultObjectType.Key, result.ObjectType);
111114
}
112115

113116
[Test]
114-
public async Task KeyVaultManagerReturnsExpectedCertificateDescriptor()
117+
[TestCase(true)]
118+
[TestCase(false)]
119+
public async Task KeyVaultManagerReturnsExpectedCertificate(bool retrieveWithPrivateKey)
115120
{
116121
this.mockDescriptor.Name = "mycert";
117-
var result = await this.keyVaultManager.GetCertificateAsync(this.mockDescriptor, CancellationToken.None, Policy.NoOpAsync());
122+
var result = await this.keyVaultManager.GetCertificateAsync(this.mockDescriptor, CancellationToken.None, retrieveWithPrivateKey);
118123
Assert.IsNotNull(result);
119-
Assert.AreEqual("mycert", result.Name);
120-
Assert.AreEqual("v3", result.Version);
121-
Assert.AreEqual(KeyVaultObjectType.Certificate, result.ObjectType);
124+
if (retrieveWithPrivateKey)
125+
{
126+
Assert.IsTrue(result.HasPrivateKey);
127+
}
128+
else
129+
{
130+
Assert.IsFalse(result.HasPrivateKey);
131+
}
122132
}
123133

124134
[Test]
@@ -186,6 +196,42 @@ public void KeyVaultManagerAppliesRetryPolicyOnTransientErrors()
186196
Assert.AreEqual(3, attempts);
187197
}
188198

199+
// Create a dummy, self-signed certificate for testing
200+
private byte[] GenerateTestCertificateBytes()
201+
{
202+
var distinguishedName = new X500DistinguishedName("CN=TestCert");
203+
204+
using var rsa = RSA.Create(2048);
205+
var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
206+
207+
var certificate = request.CreateSelfSigned(
208+
DateTimeOffset.UtcNow.AddDays(-1),
209+
DateTimeOffset.UtcNow.AddYears(1));
210+
211+
// Export to DER format (byte[]) – matches cert.Cer in Azure Key Vault
212+
return certificate.Export(X509ContentType.Cert);
213+
}
214+
215+
private X509Certificate2 GenerateTestCertificateWithPrivateKey()
216+
{
217+
var distinguishedName = new X500DistinguishedName("CN=TestWithPrivateKey");
218+
219+
using var rsa = RSA.Create(2048);
220+
var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
221+
222+
var certificate = request.CreateSelfSigned(
223+
DateTimeOffset.UtcNow.AddDays(-1),
224+
DateTimeOffset.UtcNow.AddYears(1));
225+
226+
// Export and import to ensure HasPrivateKey is true in unit tests
227+
var bytes = certificate.Export(X509ContentType.Pfx, "");
228+
#pragma warning disable SYSLIB0057
229+
// using this obsolete method just for Unit Testing
230+
var cert = new X509Certificate2(bytes, "", X509KeyStorageFlags.Exportable);
231+
#pragma warning restore SYSLIB0057
232+
return cert;
233+
}
234+
189235
private class TestKeyVaultManager : KeyVaultManager
190236
{
191237
private readonly SecretClient secretClient;

src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
namespace VirtualClient
55
{
6+
using System.Security.Cryptography.X509Certificates;
67
using System.Threading;
78
using System.Threading.Tasks;
9+
using Azure.Security.KeyVault.Keys;
810
using Polly;
911
using VirtualClient.Contracts;
1012

@@ -24,8 +26,8 @@ public interface IKeyVaultManager
2426
/// <param name="descriptor"> Provides the details for the secret to retrieve (requires "SecretName" and "VaultUri"). </param>
2527
/// <param name="cancellationToken"> A token that can be used to cancel the operation. </param>
2628
/// <param name="retryPolicy"> A policy to use for handling retries when transient errors/failures happen. </param>
27-
/// <returns> A <see cref="KeyVaultDescriptor"/> containing the secret value and metadata. </returns>
28-
Task<KeyVaultDescriptor> GetSecretAsync(
29+
/// <returns> A <see cref="string"/> containing the secret value. </returns>
30+
Task<string> GetSecretAsync(
2931
KeyVaultDescriptor descriptor,
3032
CancellationToken cancellationToken,
3133
IAsyncPolicy retryPolicy = null);
@@ -36,8 +38,8 @@ Task<KeyVaultDescriptor> GetSecretAsync(
3638
/// <param name="descriptor"> Provides the details for the key to retrieve (requires "KeyName" and "VaultUri"). </param>
3739
/// <param name="cancellationToken"> A token that can be used to cancel the operation. </param>
3840
/// <param name="retryPolicy"> A policy to use for handling retries when transient errors/failures happen. </param>
39-
/// <returns> A <see cref="KeyVaultDescriptor"/> containing the key metadata. </returns>
40-
Task<KeyVaultDescriptor> GetKeyAsync(
41+
/// <returns> A <see cref="KeyVaultKey"/> containing the key properties. </returns>
42+
Task<KeyVaultKey> GetKeyAsync(
4143
KeyVaultDescriptor descriptor,
4244
CancellationToken cancellationToken,
4345
IAsyncPolicy retryPolicy = null);
@@ -47,11 +49,13 @@ Task<KeyVaultDescriptor> GetKeyAsync(
4749
/// </summary>
4850
/// <param name="descriptor"> Provides the details for the certificate to retrieve (requires "CertificateName" and "VaultUri"). </param>
4951
/// <param name="cancellationToken"> A token that can be used to cancel the operation. </param>
52+
/// <param name="retrieveWithPrivateKey"> Indicates whether the private key should be retrieved along with the certificate. Default is false </param>
5053
/// <param name="retryPolicy"> A policy to use for handling retries when transient errors/failures happen. </param>
51-
/// <returns> A <see cref="KeyVaultDescriptor"/> containing the certificate metadata. </returns>
52-
Task<KeyVaultDescriptor> GetCertificateAsync(
54+
/// <returns> A <see cref="X509Certificate2"/> containing the requested certificate. </returns>
55+
Task<X509Certificate2> GetCertificateAsync(
5356
KeyVaultDescriptor descriptor,
5457
CancellationToken cancellationToken,
58+
bool retrieveWithPrivateKey = false,
5559
IAsyncPolicy retryPolicy = null);
5660
}
5761
}

src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ namespace VirtualClient
55
{
66
using System;
77
using System.Net;
8-
using System.Runtime.ConstrainedExecution;
8+
using System.Security.Cryptography.X509Certificates;
99
using System.Threading;
1010
using System.Threading.Tasks;
1111
using Azure;
1212
using Azure.Core;
1313
using Azure.Security.KeyVault.Certificates;
1414
using Azure.Security.KeyVault.Keys;
1515
using Azure.Security.KeyVault.Secrets;
16-
using Microsoft.CodeAnalysis.CSharp.Syntax;
1716
using Polly;
1817
using VirtualClient.Common.Extensions;
1918
using VirtualClient.Contracts;
@@ -59,12 +58,12 @@ public KeyVaultManager(DependencyKeyVaultStore storeDescription)
5958
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
6059
/// <param name="retryPolicy">A policy to use for handling retries when transient errors/failures happen.</param>
6160
/// <returns>
62-
/// A <see cref="KeyVaultDescriptor"/> containing the secret value and metadata.
61+
/// A <see cref="string"/> containing the secret value.
6362
/// </returns>
6463
/// <exception cref="DependencyException">
6564
/// Thrown if the secret is not found, access is denied, or another error occurs.
6665
/// </exception>
67-
public async Task<KeyVaultDescriptor> GetSecretAsync(
66+
public async Task<string> GetSecretAsync(
6867
KeyVaultDescriptor descriptor,
6968
CancellationToken cancellationToken,
7069
IAsyncPolicy retryPolicy = null)
@@ -87,16 +86,7 @@ public async Task<KeyVaultDescriptor> GetSecretAsync(
8786
return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
8887
{
8988
KeyVaultSecret secret = await client.GetSecretAsync(secretName, cancellationToken: cancellationToken);
90-
KeyVaultDescriptor result = new KeyVaultDescriptor(descriptor)
91-
{
92-
Value = secret.Value,
93-
Version = secret.Properties.Version,
94-
Name = secretName,
95-
VaultUri = vaultUri.ToString(),
96-
ObjectId = secret.Id?.ToString(),
97-
ObjectType = KeyVaultObjectType.Secret
98-
};
99-
return result;
89+
return secret.Value;
10090
}).ConfigureAwait(false);
10191
}
10292
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Forbidden)
@@ -136,12 +126,12 @@ public async Task<KeyVaultDescriptor> GetSecretAsync(
136126
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
137127
/// <param name="retryPolicy">A policy to use for handling retries when transient errors/failures happen.</param>
138128
/// <returns>
139-
/// A <see cref="KeyVaultDescriptor"/> containing the key metadata.
129+
/// A <see cref="KeyVaultKey"/> containing the key.
140130
/// </returns>
141131
/// <exception cref="DependencyException">
142132
/// Thrown if the key is not found, access is denied, or another error occurs.
143133
/// </exception>
144-
public async Task<KeyVaultDescriptor> GetKeyAsync(
134+
public async Task<KeyVaultKey> GetKeyAsync(
145135
KeyVaultDescriptor descriptor,
146136
CancellationToken cancellationToken,
147137
IAsyncPolicy retryPolicy = null)
@@ -162,15 +152,7 @@ public async Task<KeyVaultDescriptor> GetKeyAsync(
162152
return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
163153
{
164154
KeyVaultKey key = await client.GetKeyAsync(keyName, cancellationToken: cancellationToken);
165-
KeyVaultDescriptor result = new KeyVaultDescriptor(descriptor)
166-
{
167-
ObjectType = KeyVaultObjectType.Key,
168-
Name = keyName,
169-
VaultUri = vaultUri.ToString(),
170-
Version = key.Properties.Version,
171-
ObjectId = key.Id.ToString()
172-
};
173-
return result;
155+
return key;
174156
}).ConfigureAwait(false);
175157
}
176158
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Forbidden)
@@ -208,16 +190,18 @@ public async Task<KeyVaultDescriptor> GetKeyAsync(
208190
/// </summary>
209191
/// <param name="descriptor">Provides the details for the certificate to retrieve (requires "CertificateName" and "VaultUri").</param>
210192
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
193+
/// <param name="retrieveWithPrivateKey">flag to decode whether to retrieve certificate with private key</param>
211194
/// <param name="retryPolicy">A policy to use for handling retries when transient errors/failures happen.</param>
212195
/// <returns>
213-
/// A <see cref="KeyVaultDescriptor"/> containing the certificate metadata.
196+
/// A <see cref="X509Certificate2"/> containing the certificate
214197
/// </returns>
215198
/// <exception cref="DependencyException">
216199
/// Thrown if the certificate is not found, access is denied, or another error occurs.
217200
/// </exception>
218-
public async Task<KeyVaultDescriptor> GetCertificateAsync(
201+
public async Task<X509Certificate2> GetCertificateAsync(
219202
KeyVaultDescriptor descriptor,
220203
CancellationToken cancellationToken,
204+
bool retrieveWithPrivateKey = false,
221205
IAsyncPolicy retryPolicy = null)
222206
{
223207
this.ValidateKeyVaultStore();
@@ -235,17 +219,26 @@ public async Task<KeyVaultDescriptor> GetCertificateAsync(
235219
{
236220
return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
237221
{
238-
KeyVaultCertificateWithPolicy cert = await client.GetCertificateAsync(certName, cancellationToken: cancellationToken);
239-
KeyVaultDescriptor result = new KeyVaultDescriptor(descriptor)
222+
// Get the full certificate with private key (PFX) if requested
223+
if (retrieveWithPrivateKey)
224+
{
225+
X509Certificate2 privateKeyCert = await client
226+
.DownloadCertificateAsync(certName, cancellationToken: cancellationToken)
227+
.ConfigureAwait(false);
228+
229+
if (privateKeyCert is null || !privateKeyCert.HasPrivateKey)
230+
{
231+
throw new DependencyException("Failed to retrieve certificate content with private key.");
232+
}
233+
234+
return privateKeyCert;
235+
}
236+
else
240237
{
241-
ObjectType = KeyVaultObjectType.Certificate,
242-
Name = certName,
243-
VaultUri = vaultUri.ToString(),
244-
Version = cert.Properties.Version,
245-
ObjectId = cert.Id.ToString(),
246-
Policy = cert.Policy?.ToString()
247-
};
248-
return result;
238+
// If private key not needed, load cert from PublicBytes
239+
KeyVaultCertificateWithPolicy cert = await client.GetCertificateAsync(certName, cancellationToken: cancellationToken);
240+
return X509CertificateLoader.LoadCertificate(cert.Cer);
241+
}
249242
}).ConfigureAwait(false);
250243
}
251244
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Forbidden)

0 commit comments

Comments
 (0)