TLS and Client Auth in Spring Integration - spring-integration

I'm experimenting with TLS, mutual TLS, and host verification in my app and am having some confusion with the behavior of the Integration components. I'm using a TcpNetClientConnection Factory and a TcpNetServerConnectionFactory. I don't have a solid understanding of how these components are interacting under the hood, and want to make sure my authentication is working (or not working) as expected.
#Bean
public TcpNetClientConnectionFactory tcpClientConnectionFactory() {
TcpNetClientConnectionFactory factory =
new TcpNetClientConnectionFactory(upstreamHost, upstreamPort);
factory.setTcpSocketFactorySupport(tcpSslSocketFactorySupport);
factory.setTcpSocketSupport(new DefaultTcpSocketSupport(isHostVerificationEnabled));
return factory;
}
#Bean
TcpNetServerConnectionFactory testTcpServerConnectionFactory() {
TcpNetServerConnectionFactory factory = new TcpNetServerConnectionFactory(upstreamPort);
factory.setTcpSocketFactorySupport(tcpSslSocketFactorySupport);
factory.setTcpSocketSupport(new DefaultTcpSocketSupport(isHostVerificationEnabled));
return factory;
}
#Bean
public DefaultTcpNetSSLSocketFactorySupport tcpSslSocketFactorySupport() {
TcpSSLContextSupport sslContextSupport = new DefaultTcpSSLContextSupport(keyStore, trustStore, keyStorePassword,
trustStorePassword);
return new DefaultTcpNetSSLSocketFactorySupport(sslContextSupport);
}
It seems like the only way to create a TcpSSLContextSupport is to pass it both a keystore and a truststore. Why is the keystore necessary? If my server doesn't require client auth, I shouldn't have to pass a keystore to my client connection factory. The client factory should only have to have a truststore, to verify the server it's talking to.
In regard to client auth, does the server connection factory in my configuration require client auth? According to the reference, in order to enable client auth, I would need to override the postProcessServerSocket() method. However, I was getting SSL read exceptions until I added the client's cert to my server's truststore. Why was that necessary, since I haven't enabled client auth yet?
Finally, if the client connection factory does have a private key entry in its keystore, does it automatically utilize that key when opening a connection to the server? Or only if the server mandates client auth? It seems like my client connection factory is using its private key, even though I haven't set anything to mandate client auth yet.

The server doesn't require clientAuth by default; there's an example in the documentation about how to require it.
See this test case.
If I comment out the code at line 437
server.setTcpSocketSupport(new DefaultTcpSocketSupport(false) {
#Override
public void postProcessServerSocket(ServerSocket serverSocket) {
// ((SSLServerSocket) serverSocket).setNeedClientAuth(true);
}
});
The test fails with
java.lang.AssertionError:
Expecting code to raise a throwable.
at org.springframework.integration.ip.tcp.connection.SocketSupportTests.testNetClientAndServerSSLDifferentContexts(SocketSupportTests.java:414)
assertThatExceptionOfType(MessagingException.class)
.isThrownBy(() -> testNetClientAndServerSSLDifferentContexts(true));
Yes, the default implementation requires a keystore but it can be one that is empty or doesn't contain a client key when client auth is not needed. We should probably relax that; feel free to open a GitHub issue to request it. And/Or you can simply provide your own implementation.
In the test case (that expects to fail) we use a keyStore that doesn't contain the client cert.
TcpSSLContextSupport clientSslContextSupport = new DefaultTcpSSLContextSupport(
badClient ? "server.ks" : "client.ks",
"client.truststore.ks", "secret", "secret");
Note that the server socket also has setWantsClientAuth - maybe that defaults to true which would explain what you observed (failure if you provide a cert on the client side). I haven't tested that; try setting it to false.

Related

How to get a TokenCredential from a ServiceClientCredential object?

In my application, we presently are using ServiceClientCredentials from Microsoft.Rest. We are migrating parts of our application over to start using Azure.ResourceManager's ArmClient.
Basically all of our previous application integrations into Azure were using Microsoft.Azure.ResourceManager, which exposed agents like BlobClient or SecretClient, and these all accepted ServiceClientCredentials as a valid token type.
Now, with ArmClient I need to authenticate using DefaultAzureCredential which derives from Azure.Core's TokenCredential.
Surprisingly I haven't been able to find any examples yet of how to create this TokenCredential.
DefaultAzureCredential just works on my local PC since I'm signed into Visual Studio, but not on my build pipeline where I use Certificate based auth exposed as a ServiceClientCredential.
This was easier than I thought. The fix ended up being adding a new ServiceCollection extension method and passing in IWebHostEnvironment.
I use that to determine whether running in local debug, in which case we can use DefaultAzureCredential, or whether running in prod mode, in which case we should use Certificate Based auth.
It looks somewhat like this and works like a charm.
public static IServiceCollection AddDefaultAzureToken (this IServiceCollection services, IWebHostEnvironment environment)
{
if (environment.IsDevelopment())
{
var l = new DefaultAzureCredential();
services.AddSingleton<TokenCredential, l>;
}
else
{
var certCredential= new ClientCertificateCredential(null, null, "Abc");
services.AddSingleton<TokenCredential, certCredential>;
}
return services;
}
This works since DefaultAzureCredential and ClientCertficateCredential all have a common ancestor of TokenCredential, and the L in SOLID, the Liskov Substitution principle tells us that any implementation of a class can be substituted for any other instance of that class without breaking the application.
Note: the above sample was pseudocode and may need slight changing to work in your environment and should be cleaned to match your teams coding standards.

Service Fabric reverse proxy port configurability

I'm trying to write an encapsulation to get the uri for a local reverse proxy for service fabric and I'm having a hard time deciding how I want to approach configurability for the port (known as "HttpApplicationGatewayEndpoint" in the service manifest or "reverseProxyEndpointPort" in the arm template). The best way I've thought to do it would be to call "GetClusterManifestAsync" from the fabric client and parse it from there, but I'm also not a fan of that for a few reasons. For one, the call returns a string xml blob, which isn't guarded against changes to the manifest schema. I've also not yet found a way to query the cluster manager to find out which node type I'm currently on, so if for some silly reason the cluster has multiple node types and each one has a different reverse proxy port (just being a defensive coder here), that could potentially fail. It seems like an awful lot of effort to go through to dynamically discover that port number, and I've definitely missed things in the fabric api before, so any suggestions on how to approach this issue?
Edit:
I'm seeing from the example project that it's getting the port number from a config package in the service. I would rather not have to do it that way as then I'm going to have to write a ton of boilerplate for every service that'll need to use this to read configs and pass this around. Since this is more or less a constant at runtime then it seems to me like this could be treated as such and fetched somewhere from the fabric client?
After some time spent in the object browser I was able to find the various pieces I needed to make this properly.
public class ReverseProxyPortResolver
{
/// <summary>
/// Represents the port that the current fabric node is configured
/// to use when using a reverse proxy on localhost
/// </summary>
public static AsyncLazy<int> ReverseProxyPort = new AsyncLazy<int>(async ()=>
{
//Get the cluster manifest from the fabric client & deserialize it into a hardened object
ClusterManifestType deserializedManifest;
using (var cl = new FabricClient())
{
var manifestStr = await cl.ClusterManager.GetClusterManifestAsync().ConfigureAwait(false);
var serializer = new XmlSerializer(typeof(ClusterManifestType));
using (var reader = new StringReader(manifestStr))
{
deserializedManifest = (ClusterManifestType)serializer.Deserialize(reader);
}
}
//Fetch the setting from the correct node type
var nodeType = GetNodeType();
var nodeTypeSettings = deserializedManifest.NodeTypes.Single(x => x.Name.Equals(nodeType));
return int.Parse(nodeTypeSettings.Endpoints.HttpApplicationGatewayEndpoint.Port);
});
private static string GetNodeType()
{
try
{
return FabricRuntime.GetNodeContext().NodeType;
}
catch (FabricConnectionDeniedException)
{
//this code was invoked from a non-fabric started application
//likely a unit test
return "NodeType0";
}
}
}
News to me in this investigation was that all of the schemas for any of the service fabric xml is squirreled away in an assembly named System.Fabric.Management.ServiceModel.

Google Calendar API and shared hosting issue

I'm trying to use a public Google calendar in a webpage that will need editing functionalities.
To that effect, I created the calendar and made it public. I then created a Google service account and the related client id.
I also enabled the Calendar API and added the v3 dlls to the project.
I downloaded the p12 certificate and that's when the problems start.
The call to Google goes with a X509 cert but the way the .NET framework is built is that it uses a user temp folder.
Since it's a shared host for the web server (GoDaddy), I cannot have the app pool identity modified.
As a result, I'm getting this error:
System.Security.Cryptography.CryptographicException: The system cannot
find the file specified.
when calling:
X509Certificate2 certificate = new X509Certificate2(GoogleOAuth2CertificatePath,
"notasecret", X509KeyStorageFlags.Exportable);
that cerificate var is then to be used in the google call:
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(GoogleOAuth2EmailAddress)
{
User = GoogleAccount,
Scopes = new[] { CalendarService.Scope.Calendar }
}.FromCertificate(certificate));
... but I never get that far.
Question: is there a way to make the call differently, i.e. not to use a X509 certificate but JSON instead?
Or can I get the x509 function to use a general temp location rather than a user location to which I have no access to since I can't change the identity in the app pool?
Since I'm completely stuck, any help would be appreciated.
One simple option which avoids needing to worry about file locations is to embed the certificate within your assembly. In Visual Studio, right-click on the file and show its properties. Under Build Action, pick "Embedded resource".
You should then be able to load the data with something like this:
// In a helper class somewhere...
private static byte[] LoadResourceContent(Type type, string resourceName)
{
string fullName = type.Namespace + "." + resourceName;
using (var stream = type.Assembly.GetManifestResourceStream(fullName)
{
var output = new MemoryStream();
stream.CopyTo(output);
return output.ToArray();
}
}
Then:
byte[] data = ResourceHelper.LoadResourceContent(typeof(MyType), "Certificate.p12");
var certificate = new X509Certificate2(data, "notasecret", X509KeyStorageFlags.Exportable);
Here MyType is some type which is in the same folder as your resource.
Note that there are lots of different "web" project types in .NET... depending on the exact project type you're using, you may need to tweak this.

certificateValidationMode seems to be ignored by Azure

I'm developing a solution (web app) for windows azure that uses WSFederation for authentication. Since im on azure testing phase (local tests were successful) i've decided to use the same self signed certificate i've been using for local testing.
The problem here is that i'm getting an error saying that "The X.509 certificate CN=mytestsite.com.br is not in the trusted people store. The X.509 certificate CN=mytestsite.com.br chain building failed. The certificate that was used has a trust chain that cannot be verified".
This error makes complete sense because it's a self-signed certificate, but since im on a staging enviroment (and I would absolutally hate to ask my sponsor for extra-budget for a valid certificate right now...) I would like to use the self-signed one anyway. So I changed the certificateValidationMode to "None", but I still get the same validation error... It seems that the validation mode is being ignored!!!
Does anyone knows what can I do to make things work? (buy a valid certificate would be my last shot because they're quite expensive for my budget at this point...)
I would just use the self-signed cert, use code to "deploy" it to your Trusted People store, and all should be well. This will be closer to real production use anyways (and if you use PeerTrust in production, you'll have to do the same thing there as well, even with a real certificate).
private static void CopyServerCertIntoPeopleStore()
{
var myStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
myStore.Open(OpenFlags.ReadOnly);
var peopleStore = new X509Store(StoreName.TrustedPeople, StoreLocation.CurrentUser);
var cert = myStore.Certificates.Find(
X509FindType.FindByThumbprint,
SettingFetcher.GetSetting(SettingFetcher.SettingType.ApplicationVariable, "WcfServiceCertificateThumbprint"),
true
).OfType<X509Certificate2>().First();
peopleStore.Open(OpenFlags.ReadWrite);
peopleStore.Add(cert);
}
Use something like this, just replace the SettingFetcher with RoleEnvironment.GetConfigurationSettingValue or whatever to grab the thumbprint.
I managed to make it by adding "certificateValidationMode" to the STS web.config.

Getting Azure InstanceInput endpoint port

I'm want my client to communicate with a specific WorkerRole instance, so I'm trying to use InstanceInput endpoints.
My project is based on the example provided in this question: Azure InstanceInput endpoint usage
The problem is that I don't get the external IP address + port for the actual instance, when using RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Endpoint1"].IPEndpoint;
I just get internal address with the local port (e.g. 10.x.x.x:10100). I know that I can get the public IP address via DNS lookup (xxx.cloudapp.net), but I don't have a glue how to get the correct public port for each instance.
One possible solution would be: get the instance number (from RoleEnvironment.CurrentRoleInstance.Id) and add this instance number to the FixedPortRange minimum (e.g. 10106). This would imply that the first instance will always have the port 10106, the second instance always 10107 and so on. This solution seems a bit hacky to me, since I don't know how Windows Azure assigns the instances to the ports.
Is there a better (correct) way to retrieve the public port for each instance?
Question #2:
Are there any information about the Azure Compute Emulator supporting InstanceInput endpoints? (As I already mentioned in the comments: It seems that the Azure Compute Emulator currently doesn't support InstanceInputEndpoint).
Second solution (much better):
To get the public port, the porperty PublicIPEndpoint can be used (I don't know why I didn't notice this property in the first place).
Usage: RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Endpoint1"].PublicIPEndpoint;
Warning:
The IP address in the property is unused (http://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleinstanceendpoint.publicipendpoint.aspx).
First solution:
As 'artfulmethod' already mentioned, the REST operation Get Deployment retrieves interesting information about the current deployment. Since I encountered some small annoying 'issues', I'll will provide the code for the REST client here (in case someone else is having a similiar problem):
X509Store certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certificateStore.Open(OpenFlags.ReadOnly);
string footPrint = "xxx"; // enter the footprint of the certificate you use to upload the deployment (aka Management Certificate)
X509Certificate2Collection certs =
certificateStore.Certificates.Find(X509FindType.FindByThumbprint, footPrint, false);
if (certs.Count != 1) {
// client certificate cannot be found - check footprint
}
string url = "https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>/deployments/<deployment-name>"; // replace <xxx> with actual values
try {
var request = (HttpWebRequest)WebRequest.Create(url);
request.ClientCertificates.Add(certs[0]);
request.Headers.Add("x-ms-version", "2012-03-01"); // very important, otherwise you get an HTTP 400 error, specifies in which version the response is formatted
request.Method = "GET";
var response = (HttpWebResponse)request.GetResponse(); // get response
string result = new StreamReader(response.GetResponseStream()).ReadToEnd() // get response body
} catch (Exception ex) {
// handle error
}
The string 'result' contains all the information about the deployment (format of the XML is described in section 'Response Body' # http://msdn.microsoft.com/en-us/library/windowsazure/ee460804.aspx)
To get information about your deployments, including the VIPs and public ports for your role instances, use the Get Deployment operation on the Service Management API. The response body includes an InstanceInputList.

Resources