Skip to content
Open
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
26 changes: 9 additions & 17 deletions scripts/performance/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,24 +210,16 @@ def retry_on_exception(

def get_certificates() -> list[str]:
'''
Gets the certificates from the certhelper tool and on Mac uses find-certificate.
Gets the certificates from the certhelper tool.
'''
if ismac():
certs: list[str] = []
with open("/Users/helix-runner/certs/LabCert1.pfx", "rb") as f:
certs.append(base64.b64encode(f.read()).decode())
with open("/Users/helix-runner/certs/LabCert2.pfx", "rb") as f:
certs.append(base64.b64encode(f.read()).decode())
return certs
else:
cmd_line = [(os.path.join(str(helixpayload()), 'certhelper', "CertHelper%s" % extension()))]
cert_helper = RunCommand(cmd_line, None, True, False, 0)
try:
return cert_helper.run_and_get_stdout().splitlines()
except Exception as ex:
getLogger().error("Failed to get certificates")
getLogger().error('{0}: {1}'.format(type(ex), str(ex)))
return []
cmd_line = [(os.path.join(str(helixpayload()), 'certhelper', "CertHelper%s" % extension()))]
cert_helper = RunCommand(cmd_line, None, True, False, 0)
try:
return cert_helper.run_and_get_stdout().splitlines()
except Exception as ex:
getLogger().error("Failed to get certificates")
getLogger().error('{0}: {1}'.format(type(ex), str(ex)))
return []


def __write_pipeline_variable(name: str, value: str):
Expand Down
34 changes: 27 additions & 7 deletions src/tools/CertHelper/KeyVaultCert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -21,6 +22,7 @@ public class KeyVaultCert
private readonly string _clientId = "8c4b65ef-5a73-4d5a-a298-962d4a4ef7bc";

public X509Certificate2Collection KeyVaultCertificates { get; set; }
public List<byte[]> KeyVaultCertificateBytes { get; set; }
public ILocalCert LocalCerts { get; set; }
private TokenCredential _credential { get; set; }
private CertificateClient _certClient { get; set; }
Expand All @@ -33,16 +35,28 @@ public KeyVaultCert(TokenCredential? cred = null, CertificateClient? certClient
_certClient = certClient ?? new CertificateClient(new Uri(_keyVaultUrl), _credential);
_secretClient = secretClient ?? new SecretClient(new Uri(_keyVaultUrl), _credential);
KeyVaultCertificates = new X509Certificate2Collection();
KeyVaultCertificateBytes = new List<byte[]>();
}

public async Task LoadKeyVaultCertsAsync()
public async Task LoadKeyVaultCertsAsync(bool? rawBytesOnly = null)
{
KeyVaultCertificates.Add(await FindCertificateInKeyVaultAsync(Constants.Cert1Name));
KeyVaultCertificates.Add(await FindCertificateInKeyVaultAsync(Constants.Cert2Name));
bool skipX509Load = rawBytesOnly ?? RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

LoadKeyVaultCertsAsync’s default behavior now depends on the current OS (macOS defaults to skipping X509 loading). This makes the method’s semantics non-deterministic across platforms and can cause unit tests/callers that expect KeyVaultCertificates to be populated (when calling with no arguments) to behave differently on macOS. Consider making the default consistent (e.g., always load X509) and having the caller explicitly request raw-bytes-only mode where needed.

Suggested change
bool skipX509Load = rawBytesOnly ?? RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
bool skipX509Load = rawBytesOnly ?? false;

Copilot uses AI. Check for mistakes.

if (KeyVaultCertificates.Where(c => c == null).Count() > 0)
var (cert1, bytes1) = await FindCertificateInKeyVaultAsync(Constants.Cert1Name, skipX509Load);
var (cert2, bytes2) = await FindCertificateInKeyVaultAsync(Constants.Cert2Name, skipX509Load);

KeyVaultCertificateBytes.Add(bytes1);
KeyVaultCertificateBytes.Add(bytes2);

if (!skipX509Load)
{
throw new Exception("One or more certificates not found");
KeyVaultCertificates.Add(cert1!);
KeyVaultCertificates.Add(cert2!);

if (KeyVaultCertificates.Where(c => c == null).Count() > 0)
{
throw new Exception("One or more certificates not found");
}
}
Comment on lines +41 to 60
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

LoadKeyVaultCertsAsync appends into KeyVaultCertificateBytes/KeyVaultCertificates without clearing them first. If the method is called more than once on the same KeyVaultCert instance, the collections will accumulate duplicates and ShouldRotateCerts() comparisons (counts/thumbprints) can become incorrect. Clear the collections (or create fresh ones) at the start of LoadKeyVaultCertsAsync.

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -136,7 +150,7 @@ private async Task<ClientCertificateCredential> GetCertificateCredentialAsync(st
return ccc;
}

private async Task<X509Certificate2> FindCertificateInKeyVaultAsync(string certName)
private async Task<(X509Certificate2?, byte[])> FindCertificateInKeyVaultAsync(string certName, bool rawBytesOnly = false)
{
var keyVaultCert = await _certClient.GetCertificateAsync(certName);
if(keyVaultCert.Value == null)
Expand All @@ -149,12 +163,18 @@ private async Task<X509Certificate2> FindCertificateInKeyVaultAsync(string certN
throw new Exception("Certificate secret not found in Key Vault");
}
var certBytes = Convert.FromBase64String(secret.Value.Value);

if (rawBytesOnly)
{
return (null, certBytes);
}

#if NET9_0_OR_GREATER
var cert = X509CertificateLoader.LoadPkcs12(certBytes, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
#else
var cert = new X509Certificate2(certBytes, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
#endif
return cert;
return (cert, certBytes);
}

public bool ShouldRotateCerts()
Expand Down
19 changes: 15 additions & 4 deletions src/tools/CertHelper/LocalCert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -13,19 +14,29 @@ public class LocalCert : ILocalCert
{
public X509Certificate2Collection Certificates { get; set; }
public bool RequiresBootstrap { get; private set; }
internal IX509Store LocalMachineCerts { get; set; }
internal IX509Store? LocalMachineCerts { get; set; }

public LocalCert(IX509Store? store = null)
{
LocalMachineCerts = store ?? new TestableX509Store();
Certificates = new X509Certificate2Collection();
RequiresBootstrap = false;
GetLocalCerts();

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Skip Keychain access on macOS to avoid password prompts.
// Certs are managed as files on disk instead.
RequiresBootstrap = true;
}
else
{
LocalMachineCerts = store ?? new TestableX509Store();
Comment on lines +24 to +32
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

LocalCert ignores the injected IX509Store on macOS because the OS check short-circuits before assigning LocalMachineCerts or calling GetLocalCerts(). This makes unit tests that pass a mocked store fail when executed on macOS, and reduces testability of the macOS code path. Consider honoring the injected store regardless of OS (or gating the macOS skip only when store is null), so tests and custom callers can still provide a store implementation.

Suggested change
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Skip Keychain access on macOS to avoid password prompts.
// Certs are managed as files on disk instead.
RequiresBootstrap = true;
}
else
{
LocalMachineCerts = store ?? new TestableX509Store();
if (store != null)
{
// Honor the injected store on all platforms, including macOS.
LocalMachineCerts = store;
GetLocalCerts();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Skip default Keychain access on macOS to avoid password prompts.
// Certs are managed as files on disk instead.
RequiresBootstrap = true;
}
else
{
LocalMachineCerts = new TestableX509Store();

Copilot uses AI. Check for mistakes.
GetLocalCerts();
}
}

private void GetLocalCerts()
{
foreach (var cert in LocalMachineCerts.Certificates.Find(X509FindType.FindBySubjectName, "dotnetperf.microsoft.com", false))
foreach (var cert in LocalMachineCerts!.Certificates.Find(X509FindType.FindBySubjectName, "dotnetperf.microsoft.com", false))
{
if (cert.Subject == "CN=dotnetperf.microsoft.com")
{
Expand Down
103 changes: 79 additions & 24 deletions src/tools/CertHelper/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,42 @@ static async Task<int> Main(string[] args)
await kvc.LoadKeyVaultCertsAsync();
if (kvc.ShouldRotateCerts())
{
using (var localMachineCerts = new X509Store(StoreName.My, StoreLocation.CurrentUser))
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
localMachineCerts.Open(OpenFlags.ReadWrite);
localMachineCerts.RemoveRange(kvc.LocalCerts.Certificates);
localMachineCerts.AddRange(kvc.KeyVaultCertificates);
WriteCertsToDisk(kvc.KeyVaultCertificateBytes);
}
else
{
using (var localMachineCerts = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
localMachineCerts.Open(OpenFlags.ReadWrite);
localMachineCerts.RemoveRange(kvc.LocalCerts.Certificates);
localMachineCerts.AddRange(kvc.KeyVaultCertificates);
}
}
}
var bcc = new BlobContainerClient(new Uri("https://pvscmdupload.blob.core.windows.net/certstatus"),
new ClientCertificateCredential(TENANT_ID, CERT_CLIENT_ID, kvc.KeyVaultCertificates.First(), new() {SendCertificateChain = true}));
var currentKeyValutCertThumbprints = "";
foreach (var cert in kvc.KeyVaultCertificates)
{
currentKeyValutCertThumbprints += $"[{DateTimeOffset.UtcNow}] {cert.Thumbprint}{Environment.NewLine}";
}
var blob = bcc.GetBlobClient(System.Environment.MachineName);
if (blob.Exists())
{
var result = blob.DownloadContent();
var currentBlob = result.Value.Content.ToString();
currentBlob = currentBlob + currentKeyValutCertThumbprints;
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentBlob)), overwrite: true);
}
else

if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentKeyValutCertThumbprints)), overwrite: false);
var bcc = new BlobContainerClient(new Uri("https://pvscmdupload.blob.core.windows.net/certstatus"),
new ClientCertificateCredential(TENANT_ID, CERT_CLIENT_ID, kvc.KeyVaultCertificates.First(), new() {SendCertificateChain = true}));
var currentKeyValutCertThumbprints = "";
foreach (var cert in kvc.KeyVaultCertificates)
{
currentKeyValutCertThumbprints += $"[{DateTimeOffset.UtcNow}] {cert.Thumbprint}{Environment.NewLine}";
}
var blob = bcc.GetBlobClient(System.Environment.MachineName);
if (blob.Exists())
{
var result = blob.DownloadContent();
var currentBlob = result.Value.Content.ToString();
currentBlob = currentBlob + currentKeyValutCertThumbprints;
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentBlob)), overwrite: true);
}
else
{
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentKeyValutCertThumbprints)), overwrite: false);
Comment on lines +43 to +58
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Typo in variable name: currentKeyValutCertThumbprints should be currentKeyVaultCertThumbprints for clarity and to match the KeyVault naming used elsewhere.

Suggested change
var currentKeyValutCertThumbprints = "";
foreach (var cert in kvc.KeyVaultCertificates)
{
currentKeyValutCertThumbprints += $"[{DateTimeOffset.UtcNow}] {cert.Thumbprint}{Environment.NewLine}";
}
var blob = bcc.GetBlobClient(System.Environment.MachineName);
if (blob.Exists())
{
var result = blob.DownloadContent();
var currentBlob = result.Value.Content.ToString();
currentBlob = currentBlob + currentKeyValutCertThumbprints;
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentBlob)), overwrite: true);
}
else
{
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentKeyValutCertThumbprints)), overwrite: false);
var currentKeyVaultCertThumbprints = "";
foreach (var cert in kvc.KeyVaultCertificates)
{
currentKeyVaultCertThumbprints += $"[{DateTimeOffset.UtcNow}] {cert.Thumbprint}{Environment.NewLine}";
}
var blob = bcc.GetBlobClient(System.Environment.MachineName);
if (blob.Exists())
{
var result = blob.DownloadContent();
var currentBlob = result.Value.Content.ToString();
currentBlob = currentBlob + currentKeyVaultCertThumbprints;
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentBlob)), overwrite: true);
}
else
{
blob.Upload(new MemoryStream(Encoding.UTF8.GetBytes(currentKeyVaultCertThumbprints)), overwrite: false);

Copilot uses AI. Check for mistakes.
}
}
}
catch (Exception ex)
Expand All @@ -55,13 +66,57 @@ static async Task<int> Main(string[] args)
Console.Error.WriteLine(ex.StackTrace);
}

using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
foreach(var cert in store.Certificates.Find(X509FindType.FindBySubjectName, "dotnetperf.microsoft.com", false))
ReadCertsFromDisk();
}
else
{
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
{
Console.WriteLine(Convert.ToBase64String(cert.Export(X509ContentType.Pfx)));
foreach(var cert in store.Certificates.Find(X509FindType.FindBySubjectName, "dotnetperf.microsoft.com", false))
{
Console.WriteLine(Convert.ToBase64String(cert.Export(X509ContentType.Pfx)));
}
}
}
return 0;
}

static string GetMacCertDirectory()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(home, "certs");
}

static void WriteCertsToDisk(List<byte[]> certBytes)
{
var certDir = GetMacCertDirectory();
Directory.CreateDirectory(certDir);

var certNames = new[] { Constants.Cert1Name, Constants.Cert2Name };
for (int i = 0; i < certBytes.Count && i < certNames.Length; i++)
{
var pfxPath = Path.Combine(certDir, $"{certNames[i]}.pfx");
File.WriteAllBytes(pfxPath, certBytes[i]);
Comment on lines +91 to +101
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

WriteCertsToDisk writes PFX files (including private keys) to disk but doesn’t set restrictive permissions on the directory/files. On macOS this can leave private key material readable by other users depending on umask/defaults. Consider setting the directory to user-only and setting each .pfx file mode to 0600 (or equivalent) after writing.

Suggested change
static void WriteCertsToDisk(List<byte[]> certBytes)
{
var certDir = GetMacCertDirectory();
Directory.CreateDirectory(certDir);
var certNames = new[] { Constants.Cert1Name, Constants.Cert2Name };
for (int i = 0; i < certBytes.Count && i < certNames.Length; i++)
{
var pfxPath = Path.Combine(certDir, $"{certNames[i]}.pfx");
File.WriteAllBytes(pfxPath, certBytes[i]);
static void SetMacPermissions(string path, string mode)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return;
}
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/bin/chmod",
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(mode);
psi.ArgumentList.Add(path);
using var process = System.Diagnostics.Process.Start(psi);
process?.WaitForExit();
}
catch
{
// Best-effort: ignore failures to adjust permissions.
}
}
static void WriteCertsToDisk(List<byte[]> certBytes)
{
var certDir = GetMacCertDirectory();
Directory.CreateDirectory(certDir);
// Ensure the certificate directory is only accessible by the current user on macOS.
SetMacPermissions(certDir, "700");
var certNames = new[] { Constants.Cert1Name, Constants.Cert2Name };
for (int i = 0; i < certBytes.Count && i < certNames.Length; i++)
{
var pfxPath = Path.Combine(certDir, $"{certNames[i]}.pfx");
File.WriteAllBytes(pfxPath, certBytes[i]);
// Restrict certificate file permissions to user read/write on macOS.
SetMacPermissions(pfxPath, "600");

Copilot uses AI. Check for mistakes.
Console.Error.WriteLine($"Wrote certificate to {pfxPath}");
}
Comment on lines +97 to +103
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

WriteCertsToDisk silently truncates to the smaller of certBytes.Count and the fixed certNames.Length (2). If KeyVaultCertificateBytes is unexpectedly missing a cert (or contains extra entries), the tool will succeed but output an incomplete/mismatched set of files. Consider validating that the expected number of certs is present and failing fast if it isn’t.

Copilot uses AI. Check for mistakes.
}

static void ReadCertsFromDisk()
{
var certDir = GetMacCertDirectory();
foreach (var certName in new[] { Constants.Cert1Name, Constants.Cert2Name })
{
var pfxPath = Path.Combine(certDir, $"{certName}.pfx");
if (File.Exists(pfxPath))
{
Console.WriteLine(Convert.ToBase64String(File.ReadAllBytes(pfxPath)));
}
else
{
Console.Error.WriteLine($"Certificate file not found: {pfxPath}");
}
}
Comment on lines +109 to +120
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

ReadCertsFromDisk only logs to stderr when a certificate file is missing and the process still exits 0, which causes callers (scripts/performance/common.py) to treat the run as successful but receive an incomplete cert list. Consider failing with a non-zero exit code (or otherwise signaling failure) when any expected PFX file is missing.

Copilot uses AI. Check for mistakes.
}
}
91 changes: 91 additions & 0 deletions src/tools/CertHelperTests/KeyVaultCertTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,96 @@ public async Task ShouldRotateCerts_ShouldReturnTrue_WhenNoLocalCertsExist()
// Assert
Assert.True(result);
}

[Fact]
public async Task LoadKeyVaultCertsAsync_RawBytesOnly_ShouldPopulateBytesButNotCertificates()
{
// Arrange
Mock<TokenCredential> mockTokenCred;
Mock<CertificateClient> mockCertClient;
Mock<SecretClient> mockSecretClient;
Mock<ILocalCert> mockLocalCert;
CertStoreSetup(out mockTokenCred, out mockCertClient, out mockSecretClient, out mockLocalCert);

var keyVaultCert = new KeyVaultCert(mockTokenCred.Object, mockCertClient.Object, mockSecretClient.Object, mockLocalCert.Object);

// Act
await keyVaultCert.LoadKeyVaultCertsAsync(rawBytesOnly: true);

// Assert
Assert.Equal(2, keyVaultCert.KeyVaultCertificateBytes.Count);
Assert.True(keyVaultCert.KeyVaultCertificateBytes[0].Length > 0);
Assert.True(keyVaultCert.KeyVaultCertificateBytes[1].Length > 0);
Assert.Empty(keyVaultCert.KeyVaultCertificates);
}

[Fact]
public async Task LoadKeyVaultCertsAsync_RawBytesOnly_BytesShouldBeValidPfx()
{
// Arrange
Mock<TokenCredential> mockTokenCred;
Mock<CertificateClient> mockCertClient;
Mock<SecretClient> mockSecretClient;
Mock<ILocalCert> mockLocalCert;
CertStoreSetup(out mockTokenCred, out mockCertClient, out mockSecretClient, out mockLocalCert);

var keyVaultCert = new KeyVaultCert(mockTokenCred.Object, mockCertClient.Object, mockSecretClient.Object, mockLocalCert.Object);

// Act
await keyVaultCert.LoadKeyVaultCertsAsync(rawBytesOnly: true);

// Assert - bytes should be loadable as PFX
foreach (var certBytes in keyVaultCert.KeyVaultCertificateBytes)
{
var cert = X509CertificateLoader.LoadPkcs12(certBytes, "", X509KeyStorageFlags.DefaultKeySet);
Assert.NotNull(cert);
Assert.False(string.IsNullOrEmpty(cert.Thumbprint));
}
}

[Fact]
public async Task LoadKeyVaultCertsAsync_Default_ShouldPopulateBothBytesAndCertificates()
{
// Arrange
Mock<TokenCredential> mockTokenCred;
Mock<CertificateClient> mockCertClient;
Mock<SecretClient> mockSecretClient;
Mock<ILocalCert> mockLocalCert;
CertStoreSetup(out mockTokenCred, out mockCertClient, out mockSecretClient, out mockLocalCert);

var keyVaultCert = new KeyVaultCert(mockTokenCred.Object, mockCertClient.Object, mockSecretClient.Object, mockLocalCert.Object);

// Act
await keyVaultCert.LoadKeyVaultCertsAsync(rawBytesOnly: false);

// Assert
Assert.Equal(2, keyVaultCert.KeyVaultCertificateBytes.Count);
Assert.Equal(2, keyVaultCert.KeyVaultCertificates.Count);
}

[Fact]
public async Task ShouldRotateCerts_ShouldReturnTrue_WhenBootstrapRequired()
{
// Arrange - simulates macOS scenario where LocalCert skips Keychain
Mock<TokenCredential> mockTokenCred;
Mock<CertificateClient> mockCertClient;
Mock<SecretClient> mockSecretClient;
Mock<ILocalCert> mockLocalCert;
CertStoreSetup(out mockTokenCred, out mockCertClient, out mockSecretClient, out mockLocalCert);

mockLocalCert.Setup(lc => lc.Certificates).Returns(new X509Certificate2Collection());
mockLocalCert.Setup(lc => lc.RequiresBootstrap).Returns(true);

var keyVaultCert = new KeyVaultCert(mockTokenCred.Object, mockCertClient.Object, mockSecretClient.Object, mockLocalCert.Object);

// Act
await keyVaultCert.LoadKeyVaultCertsAsync(rawBytesOnly: true);
var result = keyVaultCert.ShouldRotateCerts();

// Assert
Assert.True(result);
Assert.Equal(2, keyVaultCert.KeyVaultCertificateBytes.Count);
Assert.Empty(keyVaultCert.KeyVaultCertificates);
}
}

Loading