Skip to content

Commit

Permalink
Update certificate manager support certificates in specific directori…
Browse files Browse the repository at this point in the history
…es. Move GetLoggedInUser to PlatformSpecifics for easier use by VC framework clients.
  • Loading branch information
brdeyo committed Oct 19, 2024
1 parent b055e7d commit 6245eaf
Show file tree
Hide file tree
Showing 17 changed files with 609 additions and 97 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.16.0
1.16.1
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ await executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None
[TestCase("PERF-MEMCACHED.json")]
public async Task MemcachedMemtierWorkloadProfileExecutesTheWorkloadAsExpectedOfServerOnUnixPlatformMultiVM(string profile)
{
this.mockFixture.SystemManagement.Setup(mgr => mgr.GetLoggedInUserName())
.Returns("mockuser");
this.mockFixture.PlatformSpecifics.EnvironmentVariables.Add(
EnvironmentVariable.SUDO_USER,
"mockuser");

IEnumerable<string> expectedCommands = new List<string>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public string Username
string username = this.Parameters.GetValue<string>(nameof(MemcachedExecutor.Username), string.Empty);
if (string.IsNullOrWhiteSpace(username))
{
username = this.SystemManagement.GetLoggedInUserName();
username = this.PlatformSpecifics.GetLoggedInUser();
}

return username;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public string Username
string username = this.Parameters.GetValue<string>(nameof(NASParallelBenchClientExecutor.Username), string.Empty);
if (string.IsNullOrWhiteSpace(username))
{
username = this.systemManagement.GetLoggedInUserName();
username = this.PlatformSpecifics.GetLoggedInUser();
}

return username;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public string Username
string username = this.Parameters.GetValue<string>(nameof(SuperBenchmarkExecutor.Username), string.Empty);
if (string.IsNullOrWhiteSpace(username))
{
username = this.systemManager.GetLoggedInUserName();
username = this.PlatformSpecifics.GetLoggedInUser();
}

return username;
Expand Down
5 changes: 5 additions & 0 deletions src/VirtualClient/VirtualClient.Contracts/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ public static class EnvironmentVariable
/// Name = VC_PACKAGES_DIR
/// </summary>
public const string VC_PACKAGES_DIR = nameof(VC_PACKAGES_DIR);

/// <summary>
/// Name = VC_SUDO_USER
/// </summary>
public const string VC_SUDO_USER = nameof(VC_SUDO_USER);
}

/// <summary>
Expand Down
26 changes: 26 additions & 0 deletions src/VirtualClient/VirtualClient.Contracts/PlatformSpecifics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,32 @@ public PlatformSpecifics(PlatformID platform, Architecture architecture, string
/// </summary>
internal static bool RunningInContainer { get; set; } = PlatformSpecifics.IsRunningInContainer();

/// <summary>
/// Get the logged in user/username. On Windows systems, the user is discoverable even when running as Administrator.
/// On Linux systems, the user can be discovered using certain environment variables when running under sudo/root.
/// </summary>
public string GetLoggedInUser()
{
string loggedInUserName = Environment.UserName;
if (string.Equals(loggedInUserName, "root"))
{
loggedInUserName = this.GetEnvironmentVariable(EnvironmentVariable.SUDO_USER);
if (string.Equals(loggedInUserName, "root") || string.IsNullOrEmpty(loggedInUserName))
{
loggedInUserName = this.GetEnvironmentVariable(EnvironmentVariable.VC_SUDO_USER);
if (string.IsNullOrEmpty(loggedInUserName))
{
throw new EnvironmentSetupException(
$"Unable to determine logged in username. The expected environment variables '{EnvironmentVariable.SUDO_USER}' and " +
$"'{EnvironmentVariable.VC_SUDO_USER}' do not exist or are set to 'root' (i.e. potentially when running as sudo/root).",
ErrorReason.EnvironmentIsInsufficent);
}
}
}

return loggedInUserName;
}

/// <summary>
/// Returns the platform + architecture name used by the Virtual Client to represent a
/// common supported platform and architecture (e.g. win-x64, win-arm64, linux-x64, linux-arm64);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,208 @@ namespace VirtualClient.Identity
{
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using Moq;
using NUnit.Framework;
using VirtualClient.TestExtensions;
using VirtualClient;
using VirtualClient.TestExtensions;

[TestFixture]
[Category("Unit")]
public class CertificateManagerTest
{
private TestCertificateManager testCertificateManager;
private Fixture mockFixture;
private MockFixture mockFixture;

[SetUp]
public void InitializeTest()
{
this.mockFixture = new Fixture();
this.mockFixture = new MockFixture();
this.mockFixture.SetupCertificateMocks();
this.testCertificateManager = new TestCertificateManager();
this.testCertificateManager = new TestCertificateManager(this.mockFixture);
}

[Test]
[TestCase("AME")]
[TestCase("GBL")]
[TestCase("AME Infra CA 02")]
[TestCase("DC=AME")]
[TestCase("DC=GBL")]
[TestCase("CN=AME")]
[TestCase("CN=AME Infra CA 02")]
[TestCase("CN=AME Infra CA 02, DC=AME, DC=GBL")]
[TestCase("CN=AME Infra CA 02,DC=AME,DC=GBL")]
public void CertificateManagerSearchesSupportsARangeOfFormatsForIssuersOnCertificates(string issuer)
{
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();
Assert.True(CertificateManager.IsMatchingIssuer(certificate, issuer));
}

[Test]
[TestCase("ABC")]
[TestCase("AME Infra CA 01")]
[TestCase("DC=ABC")]
[TestCase("DC=GBB")]
[TestCase("DC=AME Infra CA 01")]
[TestCase("CN=ABC")]
[TestCase("CN=AME Infra CA 01")]
[TestCase("CN=ABC Infra CA 01, DC=AME, DC=GBL")]
[TestCase("CN=AME Infra CA 01, DC=ABC, DC=GBL")]
[TestCase("CN=AME Infra CA 01, DC=AME, DC=GBB")]
public void CertificateManagerDoesNotMismatchIssuersOnCertificates(string issuer)
{
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();
Assert.False(CertificateManager.IsMatchingIssuer(certificate, issuer));
}

[Test]
[TestCase("virtualclient")]
[TestCase("virtualclient.corp")]
[TestCase("virtualclient.corp.azure.com")]
[TestCase("CN=virtualclient.corp")]
[TestCase("CN=virtualclient.corp.azure.com")]
public void CertificateManagerSearchesSupportsARangeOfFormatsForSubjectNamesOnCertificates(string subjectName)
{
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();
Assert.True(CertificateManager.IsMatchingSubjectName(certificate, subjectName));
}

[Test]
[TestCase("virtualclients")]
[TestCase("virtualclient.azure.com")]
[TestCase("CN=virtualclient.azure.com")]
[TestCase("CN=virtualclient.other.azure.com")]
public void CertificateManagerDoesNotMismatchSubjectNamesOnCertificates(string issuer)
{
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();
Assert.False(CertificateManager.IsMatchingSubjectName(certificate, issuer));
}

[Test]
public async Task CertificateManagerSearchesTheExpectedDirectoryForCertificates()
{
this.mockFixture.Setup(PlatformID.Unix);
this.testCertificateManager = new TestCertificateManager(this.mockFixture);

string expectedDirectory = CertificateManager.DefaultUnixCertificateDirectory;
string expectedCertificateFile = this.mockFixture.Combine(expectedDirectory, "A3706B2B12D35F8B2B5F8176F7B6F18534A23FAD");
bool confirmedDir = false;
bool confirmedFile = false;

// Issuer: AME
// Subject Name: virtualclient.corp.azure.com
// Thumbprint: A3706B2B12D35F8B2B5F8176F7B6F18534A23FAD
//
// Note that this is an expired/invalid certificate so there are no security concerns. It is merely
// used for testing purposes.
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();

// Setup:
// The certificate directory exists.
this.mockFixture.Directory
.Setup(dir => dir.Exists(expectedDirectory))
.Returns(true);

// Setup:
// There are certificates in the directory.
this.mockFixture.Directory
.Setup(dir => dir.GetFiles(It.IsAny<string>()))
.Callback<string>(actualDirectory =>
{
Assert.AreEqual(expectedDirectory, actualDirectory);
confirmedDir = true;
})
.Returns(new string[] { expectedCertificateFile });

// Setup:
// The certificate content/bytes.
this.mockFixture.File
.Setup(file => file.ReadAllBytesAsync(expectedCertificateFile, It.IsAny<CancellationToken>()))
.Callback<string, CancellationToken>((actualCertificateFile, token) =>
{
Assert.AreEqual(expectedCertificateFile, actualCertificateFile);
confirmedFile = true;
})
.ReturnsAsync(certificate.RawData);

// Expectation:
// We do not need to compare the certificate properties. We just need to ensure we attempted to
// read from the expected directory and that the certificate deserializes without error.
await this.testCertificateManager.GetCertificateFromPathAsync("AME", "virtualclient.corp.azure.com", expectedDirectory);

Assert.IsTrue(confirmedDir);
Assert.IsTrue(confirmedFile);
}

[Test]
[TestCase("AME", "virtualclient.corp.azure.com")]
[TestCase("GBL", "virtualclient.corp.azure.com")]
[TestCase("AME Infra CA 02", "virtualclient")]
[TestCase("DC=AME", "corp.azure.com")]
[TestCase("DC=GBL", "azure.com")]
[TestCase("CN=AME", "virtualclient.corp.azure.com")]
[TestCase("CN=AME Infra CA 02", "CN=virtualclient.corp.azure.com")]
[TestCase("CN=AME Infra CA 02, DC=AME, DC=GBL", "CN=virtualclient.corp.azure.com")]
[TestCase("CN=AME Infra CA 02,DC=AME,DC=GBL", "CN=virtualclient.corp.azure.com")]
public async Task CertificateManagerHandlesDifferentIssuerAndSubjectNameFormats(string issuer, string subjectName)
{
this.mockFixture.Setup(PlatformID.Unix);
this.testCertificateManager = new TestCertificateManager(this.mockFixture);

string expectedDirectory = CertificateManager.DefaultUnixCertificateDirectory;
string expectedCertificateFile = this.mockFixture.Combine(expectedDirectory, "A3706B2B12D35F8B2B5F8176F7B6F18534A23FAD");
bool confirmedDir = false;
bool confirmedFile = false;

// Issuer: AME
// Subject Name: virtualclient.corp.azure.com
// Thumbprint: A3706B2B12D35F8B2B5F8176F7B6F18534A23FAD
//
// Note that this is an expired/invalid certificate so there are no security concerns. It is merely
// used for testing purposes.
X509Certificate2 certificate = this.mockFixture.Create<X509Certificate2>();

// Setup:
// The certificate directory exists.
this.mockFixture.Directory
.Setup(dir => dir.Exists(expectedDirectory))
.Returns(true);

// Setup:
// There are certificates in the directory.
this.mockFixture.Directory
.Setup(dir => dir.GetFiles(It.IsAny<string>()))
.Callback<string>(actualDirectory =>
{
Assert.AreEqual(expectedDirectory, actualDirectory);
confirmedDir = true;
})
.Returns(new string[] { expectedCertificateFile });

// Setup:
// The certificate content/bytes.
this.mockFixture.File
.Setup(file => file.ReadAllBytesAsync(expectedCertificateFile, It.IsAny<CancellationToken>()))
.Callback<string, CancellationToken>((actualCertificateFile, token) =>
{
Assert.AreEqual(expectedCertificateFile, actualCertificateFile);
confirmedFile = true;
})
.ReturnsAsync(certificate.RawData);

// Expectation:
// We do not need to compare the certificate properties. We just need to ensure we attempted to
// read from the expected directory and that the certificate deserializes without error.
await this.testCertificateManager.GetCertificateFromPathAsync(issuer, subjectName, expectedDirectory);

Assert.IsTrue(confirmedDir);
Assert.IsTrue(confirmedFile);
}

[Test]
Expand All @@ -45,7 +223,7 @@ public void CertificateManagerThrowsWhenAValidThumbprintIsNotProvidedWhenSearchi
public void CertificateManagerSearchesTheExpectedStoreForCertificates()
{
bool isExpectedStore = false;
this.testCertificateManager.OnGetCertificateFromStoreAsync = (store, thumbprint, validOnly) =>
this.testCertificateManager.OnGetCertificateFromStoreAsync = (store, thumbprint) =>
{
isExpectedStore = store.Name == StoreName.My.ToString() && store.Location == StoreLocation.LocalMachine;

Expand All @@ -64,7 +242,7 @@ public void CertificateManagerSearchesForTheExpectedCertificateInTheStore()
{
string expectedThumbprint = "c3rt1f1c4t3thum6pr1nt";
List<bool> isExpectedCertificate = new List<bool>();
this.testCertificateManager.OnGetCertificateFromStoreAsync = (store, thumbprint, validOnly) =>
this.testCertificateManager.OnGetCertificateFromStoreAsync = (store, thumbprint) =>
{
isExpectedCertificate.Add(thumbprint == expectedThumbprint);

Expand All @@ -90,7 +268,7 @@ public void CertificateManagerSearchesIssuerAndSubjectForTheExpectedCertificateI
X509Certificate2 expectedCertificate = this.mockFixture.Create<X509Certificate2>();

List<bool> isExpectedCertificate = new List<bool>();
this.testCertificateManager.OnGetCertificateByIssuerFromStoreAsync = (store, issuer, subjectName, validOnly) =>
this.testCertificateManager.OnGetCertificateByIssuerFromStoreAsync = (store, issuer, subjectName) =>
{
isExpectedCertificate.Add(issuer == expectedIssuer);
isExpectedCertificate.Add(subjectName == expectedSubjectName);
Expand All @@ -108,26 +286,6 @@ public void CertificateManagerSearchesIssuerAndSubjectForTheExpectedCertificateI
Assert.IsFalse(isExpectedCertificate.Where(result => result == false).Any());
}

[Test]
public void CertificateManagerSearchesForAnyMatchingCertificatesByDefault()
{
List<bool> allCerts = new List<bool>();
this.testCertificateManager.OnGetCertificateFromStoreAsync = (store, thumbprint, validOnly) =>
{
allCerts.Add(validOnly == false);

return this.mockFixture.Create<X509Certificate2>();
};

this.testCertificateManager.GetCertificateFromStoreAsync("c3rt1f1c4t3thum6pr1nt")
.GetAwaiter().GetResult();

this.testCertificateManager.GetCertificateFromStoreAsync("c3rt1f1c4t3thum6pr1nt", new List<StoreLocation>() { StoreLocation.CurrentUser }, StoreName.TrustedPeople)
.GetAwaiter().GetResult();

Assert.IsFalse(allCerts.Where(result => result == false).Any());
}

[Test]
public void CertificateManagerStoreThrowsOnCertificateNotFound()
{
Expand All @@ -146,23 +304,28 @@ public void CertificateManagerStoreThrowsOnCertificateNotFoundByIssuerAndSubject

private class TestCertificateManager : CertificateManager
{
public Func<X509Store, string, bool, X509Certificate2> OnGetCertificateFromStoreAsync { get; set; }
public TestCertificateManager(MockFixture mockFixture)
: base(mockFixture.FileSystem.Object)
{
}

public Func<X509Store, string, X509Certificate2> OnGetCertificateFromStoreAsync { get; set; }

public Func<X509Store, string, string, bool, X509Certificate2> OnGetCertificateByIssuerFromStoreAsync { get; set; }
public Func<X509Store, string, string, X509Certificate2> OnGetCertificateByIssuerFromStoreAsync { get; set; }

protected override Task<X509Certificate2> GetCertificateFromStoreAsync(X509Store store, string thumbprint, bool validCertificateOnly = false)
protected override Task<X509Certificate2> GetCertificateFromStoreAsync(X509Store store, string thumbprint)
{
X509Certificate2 cert = null;
return this.OnGetCertificateFromStoreAsync != null
? Task.FromResult(this.OnGetCertificateFromStoreAsync.Invoke(store, thumbprint, validCertificateOnly))
? Task.FromResult(this.OnGetCertificateFromStoreAsync.Invoke(store, thumbprint))
: Task.FromResult(cert);
}

protected override Task<X509Certificate2> GetCertificateFromStoreAsync(X509Store store, string issuer, string subject, bool validCertificateOnly = false)
protected override Task<X509Certificate2> GetCertificateFromStoreAsync(X509Store store, string issuer, string subject)
{
X509Certificate2 cert = null;
return this.OnGetCertificateByIssuerFromStoreAsync != null
? Task.FromResult(this.OnGetCertificateByIssuerFromStoreAsync.Invoke(store, issuer, subject, validCertificateOnly))
? Task.FromResult(this.OnGetCertificateByIssuerFromStoreAsync.Invoke(store, issuer, subject))
: Task.FromResult(cert);
}
}
Expand Down
Loading

0 comments on commit 6245eaf

Please sign in to comment.