Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -854,37 +854,5 @@ public void CreateKeyVaultStoreReference_ConnectionString_ThrowsOnInvalid()
"InvalidConnectionString",
this.mockFixture.CertificateManager.Object));
}

[Test]
[TestCase("https://anyvault.vault.azure.net/?cid=123456&tid=654321")]
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tid=654321")]
[TestCase("https://anypackagestorage.blob.core.windows.net?tid=654321")]
[TestCase("https://anynamespace.servicebus.windows.net?cid=123456&tid=654321")]
[TestCase("https://my-keyvault.vault.azure.net/?;tid=654321")]
public void TryParseMicrosoftEntraTenantIdReference_Uri_WorksAsExpected(string input)
{
// Arrange
Uri uri = new Uri(input);
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);

// Assert
Assert.True(result);
Assert.AreEqual("654321", actualTenantId);
}

[Test]
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tenantId=654321")]
[TestCase("https://anypackagestorage.blob.core.windows.net?miid=654321")]
[TestCase("https://my-keyvault.vault.azure.net/;cid=654321")]
public void TryParseMicrosoftEntraTenantIdReference_Uri_ReturnFalseWhenInvalid(string input)
{
// Arrange
Uri uri = new Uri(input);
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);

// Assert
Assert.IsFalse(result);
Assert.IsNull(actualTenantId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ public void SetupDefaultBehaviors()
.Setup(c => c.GetSecretAsync("mysecret", null, It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(secret, Mock.Of<Response>()));

this.secretClientMock
.Setup(c => c.GetSecretAsync(It.Is<string>(x => x == "mysecret"), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(secret, Mock.Of<Response>()));

var pfxCertificate = this.GenerateTestCertificateWithPrivateKey();
var pfxBytes = pfxCertificate.Export(X509ContentType.Pfx, "");
var pfxBase64 = Convert.ToBase64String(pfxBytes);
var certSecret = SecretModelFactory.KeyVaultSecret(
properties: SecretModelFactory.SecretProperties(
name: "mycert",
version: "v3",
vaultUri: new Uri("https://myvault.vault.azure.net/"),
id: new Uri("https://myvault.vault.azure.net/secrets/mycert/v3")),
pfxBase64);

this.secretClientMock
.Setup(c => c.GetSecretAsync(It.Is<string>(s => s == "mycert"), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(certSecret, Mock.Of<Response>()));

// Mock the key
this.keyClientMock = new Mock<KeyClient>(MockBehavior.Strict, new Uri("https://myvault.vault.azure.net/"), new MockTokenCredential());
var key = KeyModelFactory.KeyVaultKey(properties: KeyModelFactory.KeyProperties(
Expand Down Expand Up @@ -124,10 +143,14 @@ public async Task KeyVaultManagerReturnsExpectedCertificate(bool retrieveWithPri
if (retrieveWithPrivateKey)
{
Assert.IsTrue(result.HasPrivateKey);
Assert.IsNotNull(result.Export(X509ContentType.Pfx, string.Empty)); // Verifies cert is exportable with private key
this.secretClientMock.Verify(c => c.GetSecretAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
}
else
{
Assert.IsFalse(result.HasPrivateKey);
Assert.IsNotNull(result.Export(X509ContentType.Cert, string.Empty));
this.certificateClientMock.Verify(c => c.GetCertificateAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
}
}

Expand Down
20 changes: 0 additions & 20 deletions src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,26 +398,6 @@ public static bool TryParseCertificateReference(Uri uri, out string issuer, out
return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
}

/// <summary>
/// Tries to parse the Microsoft Entra reference information from the provided uri. If the uri does not contain the correctly formatted client ID
/// and tenant ID information the method will return false, and keep the two out parameters as null.
/// Ex. https://anystore.blob.core.windows.net?cid={clientId};tid={tenantId}
/// </summary>
/// <param name="uri">The uri to attempt to parse the values from.</param>
/// <param name="tenantId">The tenant ID from the Microsoft Entra reference.</param>
/// <returns>True/False if the method was able to successfully parse both the client ID and the tenant ID from the Microsoft Entra reference.</returns>
public static bool TryParseMicrosoftEntraTenantIdReference(Uri uri, out string tenantId)
{
string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");

IDictionary<string, string> queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
entry => entry.Key,
entry => entry.Value?.ToString(),
StringComparer.OrdinalIgnoreCase);

return TryGetMicrosoftEntraTenantId(queryParameters, out tenantId);
}

/// <summary>
/// Returns the endpoint by verifying package uri checks.
/// if the endpoint is a package uri without http or https protocols then append the protocol else return the endpoint value.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace VirtualClient.Identity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will need this class. This source code should just be located in the Component that is using it. If it is used in more than 1 Component, we should just use extension methods. The source code location/folder is fine. Maybe a CertificateManagerExtensions class.

{
using System.Security.Cryptography.X509Certificates;

/// <summary>
/// Certificate Loader to cleanly handle differences in .NET versions for loading certificates from byte arrays.
/// </summary>
internal static class CertificateLoaderHelper
{
internal static X509Certificate2 LoadPublic(byte[] cerBytes)
{
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadCertificate(cerBytes);
#else
return new X509Certificate2(cerBytes);
#endif
}

internal static X509Certificate2 LoadPkcs12(
byte[] pfxBytes,
string password,
X509KeyStorageFlags flags)
{
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadPkcs12(
pfxBytes,
password,
flags);
#else
return new X509Certificate2(
pfxBytes,
password,
flags);
#endif
}
}
}
48 changes: 26 additions & 22 deletions src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace VirtualClient
using Azure.Security.KeyVault.Secrets;
using Polly;
using VirtualClient.Common.Extensions;
using VirtualClient.Identity;

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

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

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

var credentials = ((DependencyKeyVaultStore)this.StoreDescription).Credentials;

CertificateClient certificateClient = this.CreateCertificateClient(vaultUri, credentials); // For public cert.
SecretClient secretClient = this.CreateSecretClient(vaultUri, credentials); // For private cert (PFX)

try
{
return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
{
// Get the full certificate with private key (PFX) if requested
if (retrieveWithPrivateKey)
{
X509Certificate2 privateKeyCert = await client
.DownloadCertificateAsync(certName, cancellationToken: cancellationToken)
.ConfigureAwait(false);
KeyVaultSecret secret = await secretClient.GetSecretAsync(certName, cancellationToken: cancellationToken);

if (secret?.Value == null)
{
throw new DependencyException($"Secret for certificate '{certName}' not found in vault '{vaultUri}'.");
}

byte[] pfxBytes = Convert.FromBase64String(secret.Value);

if (privateKeyCert is null || !privateKeyCert.HasPrivateKey)
X509Certificate2 pfxCertificate = CertificateLoaderHelper.LoadPkcs12(
pfxBytes,
string.Empty,
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);

if (!pfxCertificate.HasPrivateKey)
{
throw new DependencyException("Failed to retrieve certificate content with private key.");
throw new DependencyException($"Certificate '{certName}' does not contain a private key.");
}

return privateKeyCert;
return pfxCertificate;
}
else
{
// If private key not needed, load cert from PublicBytes
KeyVaultCertificateWithPolicy cert = await client.GetCertificateAsync(certName, cancellationToken: cancellationToken);
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadCertificate(cert.Cer);
#elif NET8_0_OR_GREATER
return new X509Certificate2(cert.Cer);
#endif
// Public certificate only
KeyVaultCertificateWithPolicy certBundle = await certificateClient.GetCertificateAsync(certName, cancellationToken: cancellationToken);
return CertificateLoaderHelper.LoadPublic(certBundle.Cer);
}
}).ConfigureAwait(false);
}
Expand All @@ -269,13 +280,6 @@ public async Task<X509Certificate2> GetCertificateAsync(
ex,
ErrorReason.HttpNonSuccessResponse);
}
catch (Exception ex)
{
throw new DependencyException(
$"Failed to get certificate '{certName}' from vault '{vaultUri}'.",
ex,
ErrorReason.HttpNonSuccessResponse);
}
}

/// <summary>
Expand Down Expand Up @@ -328,4 +332,4 @@ private void ValidateKeyVaultStore()
}
}
}
}
}
Loading
Loading