diff --git a/src/Agent.Listener/Configuration.Windows/RSAEncryptedFileKeyManager.cs b/src/Agent.Listener/Configuration.Windows/RSAEncryptedFileKeyManager.cs index d13d071c3d..d9c6806fd3 100644 --- a/src/Agent.Listener/Configuration.Windows/RSAEncryptedFileKeyManager.cs +++ b/src/Agent.Listener/Configuration.Windows/RSAEncryptedFileKeyManager.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.VisualStudio.Services.Agent.Util; +using System; using System.IO; using System.Security.Cryptography; using System.Text; -using Microsoft.VisualStudio.Services.Agent.Util; namespace Microsoft.VisualStudio.Services.Agent.Listener.Configuration { @@ -13,7 +14,61 @@ public class RSAEncryptedFileKeyManager : AgentService, IRSAKeyManager private string _keyFile; private IHostContext _context; - public RSACryptoServiceProvider CreateKey() + public RSACryptoServiceProvider CreateKey(bool enableAgentKeyStoreInNamedContainer) + { + if (enableAgentKeyStoreInNamedContainer) + { + return CreateKeyStoreKeyInNamedContainer(); + } + else + { + return CreateKeyStoreKeyInFile(); + } + } + + private RSACryptoServiceProvider CreateKeyStoreKeyInNamedContainer() + { + RSACryptoServiceProvider rsa; + if (!File.Exists(_keyFile)) + { + Trace.Info("Creating new RSA key using 2048-bit key length"); + + CspParameters Params = new CspParameters(); + Params.KeyContainerName = "AgentKeyContainer" + Guid.NewGuid().ToString(); + Params.Flags |= CspProviderFlags.UseNonExportableKey | CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(2048, Params); + + // Now write the parameters to disk + SaveParameters(default(RSAParameters), Params.KeyContainerName); + Trace.Info("Successfully saved containerName to file {0} in container {1}", _keyFile, Params.KeyContainerName); + } + else + { + Trace.Info("Found existing RSA key parameters file {0}", _keyFile); + + var result = LoadParameters(); + + if(string.IsNullOrEmpty(result.containerName)) + { + Trace.Info("Container name not present; reading RSA key from file"); + return CreateKeyStoreKeyInFile(); + } + + CspParameters Params = new CspParameters(); + Params.KeyContainerName = result.containerName; + Params.Flags |= CspProviderFlags.UseNonExportableKey | CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(Params); + } + + return rsa; + + // References: + // https://stackoverflow.com/questions/2274596/how-to-store-a-public-key-in-a-machine-level-rsa-key-container + // https://social.msdn.microsoft.com/Forums/en-US/e3902420-3a82-42cf-a4a3-de230ebcea56/how-to-store-a-public-key-in-a-machinelevel-rsa-key-container?forum=netfxbcl + // https://security.stackexchange.com/questions/234477/windows-certificates-where-is-private-key-located + } + + private RSACryptoServiceProvider CreateKeyStoreKeyInFile() { RSACryptoServiceProvider rsa = null; if (!File.Exists(_keyFile)) @@ -23,15 +78,23 @@ public RSACryptoServiceProvider CreateKey() rsa = new RSACryptoServiceProvider(2048); // Now write the parameters to disk - SaveParameters(rsa.ExportParameters(true)); + SaveParameters(rsa.ExportParameters(true), string.Empty); Trace.Info("Successfully saved RSA key parameters to file {0}", _keyFile); } else { Trace.Info("Found existing RSA key parameters file {0}", _keyFile); + var result = LoadParameters(); + + if(!string.IsNullOrEmpty(result.containerName)) + { + Trace.Info("Keyfile has ContainerName, so we must read from named container"); + return CreateKeyStoreKeyInNamedContainer(); + } + rsa = new RSACryptoServiceProvider(); - rsa.ImportParameters(LoadParameters()); + rsa.ImportParameters(result.rsaParameters); } return rsa; @@ -46,7 +109,19 @@ public void DeleteKey() } } - public RSACryptoServiceProvider GetKey() + public RSACryptoServiceProvider GetKey(bool enableAgentKeyStoreInNamedContainer) + { + if (enableAgentKeyStoreInNamedContainer) + { + return GetKeyFromNamedContainer(); + } + else + { + return GetKeyFromFile(); + } + } + + private RSACryptoServiceProvider GetKeyFromNamedContainer() { if (!File.Exists(_keyFile)) { @@ -55,21 +130,53 @@ public RSACryptoServiceProvider GetKey() Trace.Info("Loading RSA key parameters from file {0}", _keyFile); + var result = LoadParameters(); + + if (string.IsNullOrEmpty(result.containerName)) + { + return GetKeyFromFile(); + } + + CspParameters Params = new CspParameters(); + Params.KeyContainerName = result.containerName; + Params.Flags |= CspProviderFlags.UseNonExportableKey | CspProviderFlags.UseMachineKeyStore; + var rsa = new RSACryptoServiceProvider(Params); + return rsa; + } + + private RSACryptoServiceProvider GetKeyFromFile() + { + if (!File.Exists(_keyFile)) + { + throw new CryptographicException(StringUtil.Loc("RSAKeyFileNotFound", _keyFile)); + } + + Trace.Info("Loading RSA key parameters from file {0}", _keyFile); + + var result = LoadParameters(); + + if(!string.IsNullOrEmpty(result.containerName)) + { + Trace.Info("Keyfile has ContainerName, reading from NamedContainer"); + return GetKeyFromNamedContainer(); + } + var rsa = new RSACryptoServiceProvider(); - rsa.ImportParameters(LoadParameters()); + rsa.ImportParameters(result.rsaParameters); return rsa; } - private RSAParameters LoadParameters() + private (string containerName, RSAParameters rsaParameters) LoadParameters() { var encryptedBytes = File.ReadAllBytes(_keyFile); var parametersString = Encoding.UTF8.GetString(ProtectedData.Unprotect(encryptedBytes, null, DataProtectionScope.LocalMachine)); - return StringUtil.ConvertFromJson(parametersString).RSAParameters; + var deserialized = StringUtil.ConvertFromJson(parametersString); + return (deserialized.ContainerName, deserialized.RSAParameters); } - private void SaveParameters(RSAParameters parameters) + private void SaveParameters(RSAParameters parameters, string containerName) { - var parametersString = StringUtil.ConvertToJson(new RSAParametersSerializable(parameters)); + var parametersString = StringUtil.ConvertToJson(new RSAParametersSerializable(containerName, parameters)); var encryptedBytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(parametersString), null, DataProtectionScope.LocalMachine); File.WriteAllBytes(_keyFile, encryptedBytes); File.SetAttributes(_keyFile, File.GetAttributes(_keyFile) | FileAttributes.Hidden); diff --git a/src/Agent.Listener/Configuration/ConfigurationManager.cs b/src/Agent.Listener/Configuration/ConfigurationManager.cs index 2257e22a55..c3049565a7 100644 --- a/src/Agent.Listener/Configuration/ConfigurationManager.cs +++ b/src/Agent.Listener/Configuration/ConfigurationManager.cs @@ -178,11 +178,12 @@ public async Task ConfigureAsync(CommandSettings command) _term.WriteError(StringUtil.Loc("FailedToConnect")); } } - + // We want to use the native CSP of the platform for storage, so we use the RSACSP directly RSAParameters publicKey; var keyManager = HostContext.GetService(); - using (var rsa = keyManager.CreateKey()) + var enableAgentKeyStoreInNamedContainer = await keyManager.GetStoreAgentTokenInNamedContainerFF(HostContext, Trace, agentSettings, creds); + using (var rsa = keyManager.CreateKey(enableAgentKeyStoreInNamedContainer)) { publicKey = rsa.ExportParameters(false); } diff --git a/src/Agent.Listener/Configuration/FeatureFlagProvider.cs b/src/Agent.Listener/Configuration/FeatureFlagProvider.cs index e603dc24ae..9c1503ceae 100644 --- a/src/Agent.Listener/Configuration/FeatureFlagProvider.cs +++ b/src/Agent.Listener/Configuration/FeatureFlagProvider.cs @@ -28,6 +28,7 @@ public interface IFeatureFlagProvider : IAgentService /// Thrown if agent is not configured public Task GetFeatureFlagAsync(IHostContext context, string featureFlagName, ITraceWriter traceWriter, CancellationToken ctk = default); + public Task GetFeatureFlagWithCred(IHostContext context, string featureFlagName, ITraceWriter traceWriter, AgentSettings settings, VssCredentials creds, CancellationToken ctk = default); } public class FeatureFlagProvider : AgentService, IFeatureFlagProvider @@ -40,22 +41,33 @@ public async Task GetFeatureFlagAsync(IHostContext context, string ArgUtil.NotNull(featureFlagName, nameof(featureFlagName)); var credMgr = context.GetService(); + VssCredentials creds = credMgr.LoadCredentials(); var configManager = context.GetService(); + AgentSettings settings = configManager.LoadSettings(); + + return await GetFeatureFlagWithCred(context, featureFlagName, traceWriter, settings, creds, ctk); + } + + public async Task GetFeatureFlagWithCred(IHostContext context, string featureFlagName, + ITraceWriter traceWriter, AgentSettings settings, VssCredentials creds, CancellationToken ctk) + { var agentCertManager = context.GetService(); - VssCredentials creds = credMgr.LoadCredentials(); ArgUtil.NotNull(creds, nameof(creds)); - AgentSettings settings = configManager.LoadSettings(); using var vssConnection = VssUtil.CreateConnection(new Uri(settings.ServerUrl), creds, traceWriter, agentCertManager.SkipServerCertificateValidation); var client = vssConnection.GetClient(); try { - return await client.GetFeatureFlagByNameAsync(featureFlagName, checkFeatureExists: false); - } catch (VssServiceException e) { + return await client.GetFeatureFlagByNameAsync(featureFlagName, checkFeatureExists: false, ctk); + } + catch (VssServiceException e) + { Trace.Warning("Unable to retrieve feature flag status: " + e.ToString()); return new FeatureFlag(featureFlagName, "", "", "Off", "Off"); - } catch (VssUnauthorizedException e) { + } + catch (VssUnauthorizedException e) + { Trace.Warning("Unable to retrieve feature flag with following exception: " + e.ToString()); return new FeatureFlag(featureFlagName, "", "", "Off", "Off"); } diff --git a/src/Agent.Listener/Configuration/IRSAKeyManager.cs b/src/Agent.Listener/Configuration/IRSAKeyManager.cs index eb69b96702..e4c2cd60e0 100644 --- a/src/Agent.Listener/Configuration/IRSAKeyManager.cs +++ b/src/Agent.Listener/Configuration/IRSAKeyManager.cs @@ -1,9 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Agent.Listener.Configuration; +using Agent.Sdk.Knob; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Common; using System; using System.Runtime.Serialization; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.VisualStudio.Services.Agent.Listener.Configuration { @@ -21,7 +27,7 @@ public interface IRSAKeyManager : IAgentService /// key is returned to the caller. /// /// An RSACryptoServiceProvider instance representing the key for the agent - RSACryptoServiceProvider CreateKey(); + RSACryptoServiceProvider CreateKey(bool enableAgentKeyStoreInNamedContainer); /// /// Deletes the RSA key managed by the key manager. @@ -33,7 +39,23 @@ public interface IRSAKeyManager : IAgentService /// /// An RSACryptoServiceProvider instance representing the key for the agent /// No key exists in the store - RSACryptoServiceProvider GetKey(); + RSACryptoServiceProvider GetKey(bool enableAgentKeyStoreInNamedContainer); + } + + public static class IRSAKeyManagerExtensions + { + public static async Task GetStoreAgentTokenInNamedContainerFF(this IRSAKeyManager rsaKeyManager, IHostContext hostContext, global::Agent.Sdk.ITraceWriter trace, AgentSettings agentSettings, VssCredentials creds, CancellationToken cancellationToken = default) + { + if(AgentKnobs.StoreAgentKeyInCSPContainer.GetValue(UtilKnobValueContext.Instance()).AsBoolean()) + { + return true; + } + + var featureFlagProvider = hostContext.GetService(); + var enableAgentKeyStoreInNamedContainer = (await featureFlagProvider.GetFeatureFlagWithCred(hostContext, "DistributedTask.Agent.StoreAgentTokenInNamedContainer", trace, agentSettings, creds, cancellationToken)).EffectiveState == "On"; + + return enableAgentKeyStoreInNamedContainer; + } } // Newtonsoft 10 is not working properly with dotnet RSAParameters class @@ -44,6 +66,7 @@ public interface IRSAKeyManager : IAgentService [Serializable] internal class RSAParametersSerializable : ISerializable { + private string _containerName; private RSAParameters _rsaParameters; public RSAParameters RSAParameters @@ -54,8 +77,9 @@ public RSAParameters RSAParameters } } - public RSAParametersSerializable(RSAParameters rsaParameters) + public RSAParametersSerializable(string containerName, RSAParameters rsaParameters) { + _containerName = containerName; _rsaParameters = rsaParameters; } @@ -63,6 +87,8 @@ private RSAParametersSerializable() { } + public string ContainerName { get { return _containerName; } set { _containerName = value; } } + public byte[] D { get { return _rsaParameters.D; } set { _rsaParameters.D = value; } } public byte[] DP { get { return _rsaParameters.DP; } set { _rsaParameters.DP = value; } } @@ -81,6 +107,8 @@ private RSAParametersSerializable() public RSAParametersSerializable(SerializationInfo information, StreamingContext context) { + _containerName = (string)information.GetValue("ContainerName", typeof(string)); + _rsaParameters = new RSAParameters() { D = (byte[])information.GetValue("d", typeof(byte[])), @@ -96,6 +124,7 @@ public RSAParametersSerializable(SerializationInfo information, StreamingContext public void GetObjectData(SerializationInfo info, StreamingContext context) { + info.AddValue("ContainerName", _containerName); info.AddValue("d", _rsaParameters.D); info.AddValue("dp", _rsaParameters.DP); info.AddValue("dq", _rsaParameters.DQ); diff --git a/src/Agent.Listener/Configuration/OAuthCredential.cs b/src/Agent.Listener/Configuration/OAuthCredential.cs index 82dc71780c..2272c29a31 100644 --- a/src/Agent.Listener/Configuration/OAuthCredential.cs +++ b/src/Agent.Listener/Configuration/OAuthCredential.cs @@ -42,7 +42,7 @@ public override VssCredentials GetVssCredentials(IHostContext context) // We expect the key to be in the machine store at this point. Configuration should have set all of // this up correctly so we can use the key to generate access tokens. var keyManager = context.GetService(); - var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey()); + var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey(enableAgentKeyStoreInNamedContainer: true)); var clientCredential = new VssOAuthJwtBearerClientCredential(clientId, authorizationUrl, signingCredentials); var agentCredential = new VssOAuthCredential(new Uri(oathEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential); diff --git a/src/Agent.Listener/Configuration/RSAFileKeyManager.cs b/src/Agent.Listener/Configuration/RSAFileKeyManager.cs index 6f5f7e45d8..f053bfc84a 100644 --- a/src/Agent.Listener/Configuration/RSAFileKeyManager.cs +++ b/src/Agent.Listener/Configuration/RSAFileKeyManager.cs @@ -14,7 +14,7 @@ public class RSAFileKeyManager : AgentService, IRSAKeyManager private string _keyFile; private IHostContext _context; - public RSACryptoServiceProvider CreateKey() + public RSACryptoServiceProvider CreateKey(bool enableAgentKeyStoreInNamedContainer) { RSACryptoServiceProvider rsa = null; if (!File.Exists(_keyFile)) @@ -24,7 +24,7 @@ public RSACryptoServiceProvider CreateKey() rsa = new RSACryptoServiceProvider(2048); // Now write the parameters to disk - IOUtil.SaveObject(new RSAParametersSerializable(rsa.ExportParameters(true)), _keyFile); + IOUtil.SaveObject(new RSAParametersSerializable("", rsa.ExportParameters(true)), _keyFile); Trace.Info("Successfully saved RSA key parameters to file {0}", _keyFile); // Try to lock down the credentials_key file to the owner/group @@ -70,7 +70,7 @@ public void DeleteKey() } } - public RSACryptoServiceProvider GetKey() + public RSACryptoServiceProvider GetKey(bool enableAgentKeyStoreInNamedContainer) { if (!File.Exists(_keyFile)) { diff --git a/src/Agent.Listener/MessageListener.cs b/src/Agent.Listener/MessageListener.cs index 16ae691aa4..aee4522f10 100644 --- a/src/Agent.Listener/MessageListener.cs +++ b/src/Agent.Listener/MessageListener.cs @@ -38,6 +38,7 @@ public sealed class MessageListener : AgentService, IMessageListener private IAgentServer _agentServer; private TaskAgentSession _session; private TimeSpan _getNextMessageRetryInterval; + private bool _storeAgentKeyInCSPContainer; private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30); private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4); private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30); @@ -99,6 +100,9 @@ public async Task CreateSessionAsync(CancellationToken token) taskAgentSession, token); + var keyManager = HostContext.GetService(); + _storeAgentKeyInCSPContainer = await keyManager.GetStoreAgentTokenInNamedContainerFF(HostContext, Trace, _settings, creds); + Trace.Info($"Session created."); if (encounteringError) { @@ -317,7 +321,7 @@ private ICryptoTransform GetMessageDecryptor( { // The agent session encryption key uses the AES symmetric algorithm var keyManager = HostContext.GetService(); - using (var rsa = keyManager.GetKey()) + using (var rsa = keyManager.GetKey(_storeAgentKeyInCSPContainer)) { return aes.CreateDecryptor(rsa.Decrypt(_session.EncryptionKey.Value, RSAEncryptionPadding.OaepSHA1), message.IV); } diff --git a/src/Agent.Sdk/Knob/AgentKnobs.cs b/src/Agent.Sdk/Knob/AgentKnobs.cs index cc95440fb3..e1ffd91ec4 100644 --- a/src/Agent.Sdk/Knob/AgentKnobs.cs +++ b/src/Agent.Sdk/Knob/AgentKnobs.cs @@ -580,5 +580,11 @@ public class AgentKnobs new RuntimeKnobSource("AZP_AGENT_DOCKER_INIT_OPTION"), new EnvironmentKnobSource("AZP_AGENT_DOCKER_INIT_OPTION"), new BuiltInDefaultKnobSource("false")); + + public static readonly Knob StoreAgentKeyInCSPContainer = new Knob( + nameof(StoreAgentKeyInCSPContainer), + "Store agent key in named container (Windows).", + new EnvironmentKnobSource("STORE_AGENT_KEY_IN_CSP_CONTAINER"), + new BuiltInDefaultKnobSource("false")); } } diff --git a/src/Test/L0/Listener/Configuration/ConfigurationManagerL0.cs b/src/Test/L0/Listener/Configuration/ConfigurationManagerL0.cs index 372214d12d..c4e29766f6 100644 --- a/src/Test/L0/Listener/Configuration/ConfigurationManagerL0.cs +++ b/src/Test/L0/Listener/Configuration/ConfigurationManagerL0.cs @@ -17,6 +17,9 @@ using Xunit; using Microsoft.VisualStudio.Services.Location; using Microsoft.VisualStudio.Services.Common; +using Agent.Listener.Configuration; +using Agent.Sdk; +using System.Threading; namespace Microsoft.VisualStudio.Services.Agent.Tests.Listener.Configuration { @@ -38,6 +41,7 @@ public sealed class ConfigurationManagerL0 : IDisposable private Mock _macServiceControlManager; private Mock _rsaKeyManager; + private Mock _featureFlagProvider; private ICapabilitiesManager _capabilitiesManager; private DeploymentGroupAgentConfigProvider _deploymentGroupAgentConfigProvider; private string _expectedToken = "expectedToken"; @@ -75,6 +79,7 @@ public ConfigurationManagerL0() _linuxServiceControlManager = new Mock(); _macServiceControlManager = new Mock(); _capabilitiesManager = new CapabilitiesManager(); + _featureFlagProvider = new Mock(); var expectedAgent = new TaskAgent(_expectedAgentName) { Id = 1 }; var expectedDeploymentMachine = new DeploymentMachine() { Agent = expectedAgent, Id = _expectedDeploymentMachineId }; @@ -126,7 +131,9 @@ public ConfigurationManagerL0() rsa = new RSACryptoServiceProvider(2048); - _rsaKeyManager.Setup(x => x.CreateKey()).Returns(rsa); + _rsaKeyManager.Setup(x => x.CreateKey(It.IsAny())).Returns(rsa); + + _featureFlagProvider.Setup(x => x.GetFeatureFlagWithCred(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FeatureAvailability.FeatureFlag("", "", "", "Off", "Off"))); } private TestHostContext CreateTestContext([CallerMemberName] String testName = "") @@ -149,6 +156,7 @@ private TestHostContext CreateTestContext([CallerMemberName] String testName = " tc.SetSingleton(_macServiceControlManager.Object); tc.SetSingleton(_rsaKeyManager.Object); + tc.SetSingleton(_featureFlagProvider.Object); return tc; } diff --git a/src/Test/L0/Listener/MessageListenerL0.cs b/src/Test/L0/Listener/MessageListenerL0.cs index 44edd01ad5..d37b008017 100644 --- a/src/Test/L0/Listener/MessageListenerL0.cs +++ b/src/Test/L0/Listener/MessageListenerL0.cs @@ -1,13 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Agent.Listener.Configuration; +using Agent.Sdk; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Microsoft.VisualStudio.Services.Agent.Listener; using Microsoft.VisualStudio.Services.Agent.Capabilities; using Microsoft.VisualStudio.Services.Agent.Listener.Configuration; +using Microsoft.VisualStudio.Services.Common; using Moq; using System; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Threading.Tasks; using Xunit; using System.Threading; @@ -16,13 +20,16 @@ namespace Microsoft.VisualStudio.Services.Agent.Tests.Listener { - public sealed class MessageListenerL0 + public sealed class MessageListenerL0 : IDisposable { private AgentSettings _settings; private Mock _config; private Mock _agentServer; private Mock _credMgr; private Mock _capabilitiesManager; + private Mock _featureFlagProvider; + private Mock _rsaKeyManager; + private readonly RSACryptoServiceProvider rsa; public MessageListenerL0() { @@ -32,6 +39,14 @@ public MessageListenerL0() _agentServer = new Mock(); _credMgr = new Mock(); _capabilitiesManager = new Mock(); + _featureFlagProvider = new Mock(); + _rsaKeyManager = new Mock(); + + _featureFlagProvider.Setup(x => x.GetFeatureFlagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FeatureAvailability.FeatureFlag("", "", "", "Off", "Off"))); + _featureFlagProvider.Setup(x => x.GetFeatureFlagWithCred(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FeatureAvailability.FeatureFlag("", "", "", "Off", "Off"))); + + rsa = new RSACryptoServiceProvider(2048); + _rsaKeyManager.Setup(x => x.CreateKey(It.IsAny())).Returns(rsa); } private TestHostContext CreateTestContext([CallerMemberName] String testName = "") @@ -41,6 +56,8 @@ private TestHostContext CreateTestContext([CallerMemberName] String testName = " tc.SetSingleton(_agentServer.Object); tc.SetSingleton(_credMgr.Object); tc.SetSingleton(_capabilitiesManager.Object); + tc.SetSingleton(_featureFlagProvider.Object); + tc.SetSingleton(_rsaKeyManager.Object); return tc; } @@ -211,5 +228,10 @@ public async void GetNextMessage() _settings.PoolId, expectedSession.SessionId, It.IsAny(), tokenSource.Token), Times.Exactly(arMessages.Length)); } } + + public void Dispose() + { + rsa.Dispose(); + } } }