Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented option to store token in named container #4607

Merged
merged 5 commits into from Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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
{
Expand All @@ -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))
Expand All @@ -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;
Expand All @@ -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))
{
Expand All @@ -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<RSAParametersSerializable>(parametersString).RSAParameters;
var deserialized = StringUtil.ConvertFromJson<RSAParametersSerializable>(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);
Expand Down
5 changes: 3 additions & 2 deletions src/Agent.Listener/Configuration/ConfigurationManager.cs
Expand Up @@ -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<IRSAKeyManager>();
using (var rsa = keyManager.CreateKey())
var enableAgentKeyStoreInNamedContainer = await keyManager.GetStoreAgentTokenInNamedContainerFF(HostContext, Trace, agentSettings, creds);
using (var rsa = keyManager.CreateKey(enableAgentKeyStoreInNamedContainer))
{
publicKey = rsa.ExportParameters(false);
}
Expand Down
22 changes: 17 additions & 5 deletions src/Agent.Listener/Configuration/FeatureFlagProvider.cs
Expand Up @@ -28,6 +28,7 @@ public interface IFeatureFlagProvider : IAgentService
/// <exception cref="InvalidOperationException">Thrown if agent is not configured</exception>
public Task<FeatureFlag> GetFeatureFlagAsync(IHostContext context, string featureFlagName, ITraceWriter traceWriter, CancellationToken ctk = default);

public Task<FeatureFlag> GetFeatureFlagWithCred(IHostContext context, string featureFlagName, ITraceWriter traceWriter, AgentSettings settings, VssCredentials creds, CancellationToken ctk = default);
}

public class FeatureFlagProvider : AgentService, IFeatureFlagProvider
Expand All @@ -40,22 +41,33 @@ public class FeatureFlagProvider : AgentService, IFeatureFlagProvider
ArgUtil.NotNull(featureFlagName, nameof(featureFlagName));

var credMgr = context.GetService<ICredentialManager>();
VssCredentials creds = credMgr.LoadCredentials();
var configManager = context.GetService<IConfigurationManager>();
AgentSettings settings = configManager.LoadSettings();

return await GetFeatureFlagWithCred(context, featureFlagName, traceWriter, settings, creds, ctk);
}

public async Task<FeatureFlag> GetFeatureFlagWithCred(IHostContext context, string featureFlagName,
ITraceWriter traceWriter, AgentSettings settings, VssCredentials creds, CancellationToken ctk)
{
var agentCertManager = context.GetService<IAgentCertificateManager>();

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<FeatureAvailabilityHttpClient>();
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");
}
Expand Down
35 changes: 32 additions & 3 deletions 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
{
Expand All @@ -21,7 +27,7 @@ public interface IRSAKeyManager : IAgentService
/// key is returned to the caller.
/// </summary>
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the agent</returns>
RSACryptoServiceProvider CreateKey();
RSACryptoServiceProvider CreateKey(bool enableAgentKeyStoreInNamedContainer);

/// <summary>
/// Deletes the RSA key managed by the key manager.
Expand All @@ -33,7 +39,23 @@ public interface IRSAKeyManager : IAgentService
/// </summary>
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the agent</returns>
/// <exception cref="CryptographicException">No key exists in the store</exception>
RSACryptoServiceProvider GetKey();
RSACryptoServiceProvider GetKey(bool enableAgentKeyStoreInNamedContainer);
}

public static class IRSAKeyManagerExtensions
{
public static async Task<bool> 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<IFeatureFlagProvider>();
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
Expand All @@ -44,6 +66,7 @@ public interface IRSAKeyManager : IAgentService
[Serializable]
internal class RSAParametersSerializable : ISerializable
{
private string _containerName;
private RSAParameters _rsaParameters;

public RSAParameters RSAParameters
Expand All @@ -54,15 +77,18 @@ public RSAParameters RSAParameters
}
}

public RSAParametersSerializable(RSAParameters rsaParameters)
public RSAParametersSerializable(string containerName, RSAParameters rsaParameters)
{
_containerName = containerName;
_rsaParameters = rsaParameters;
}

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; } }
Expand All @@ -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[])),
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Agent.Listener/Configuration/OAuthCredential.cs
Expand Up @@ -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<IRSAKeyManager>();
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);

Expand Down
6 changes: 3 additions & 3 deletions src/Agent.Listener/Configuration/RSAFileKeyManager.cs
Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -70,7 +70,7 @@ public void DeleteKey()
}
}

public RSACryptoServiceProvider GetKey()
public RSACryptoServiceProvider GetKey(bool enableAgentKeyStoreInNamedContainer)
{
if (!File.Exists(_keyFile))
{
Expand Down