Windows/Linux dotnet core SslStream differences - c#

I'm trying to get SslStream with local (file) certificates working with dotnet core 5. On Linux (Alpine Linux 3.14.0), everything functions as intended with servers authenticating the remote client. On Windows (Windows 10 Enterprise, version 20H2), it seems that the authentication procedure is still trying to use the Windows certificate store to validate even though the certificate validation should be overridden by the SslStream constructor.
Is this a bug in the Windows implementation of SslStream, or am I missing a required configuration to force it to only use the loaded certificate files?
Test program below. The program will generate a CA and certificates for a client and server. It will then create 2 threads to test a SslStream using those certificates. Linux runs without any issues, but Windows will throw a System.ComponentModel.Win32Exception (0x80090304): The Local Security Authority cannot be contacted when it runs.
using System;
namespace sslstream
{
class Program
{
static bool VerifyCertificate(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
return true; //TODO: verify certificate chain and hostnames
}
static void RunClient()
{
var clientCert = new System.Security.Cryptography.X509Certificates.X509Certificate2("Client.pfx");
var collection = new System.Security.Cryptography.X509Certificates.X509CertificateCollection(new System.Security.Cryptography.X509Certificates.X509Certificate[] { clientCert });
using var client = new System.Net.Sockets.TcpClient();
client.Connect(System.Net.IPAddress.Loopback, 12345);
var clientStream = client.GetStream();
using var sStream = new System.Net.Security.SslStream(clientStream, false, new System.Net.Security.RemoteCertificateValidationCallback(VerifyCertificate), null, System.Net.Security.EncryptionPolicy.RequireEncryption);
sStream.AuthenticateAsClient("127.0.0.1", collection, System.Security.Authentication.SslProtocols.Tls12, false);
sStream.Write(new byte[1] { 55 });
}
static void RunServer()
{
var serverCert = new System.Security.Cryptography.X509Certificates.X509Certificate2("127.0.0.1.pfx");
var listener = new System.Net.Sockets.TcpListener(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 12345));
listener.Start();
using var client = listener.AcceptTcpClient();
var clientStream = client.GetStream();
using var sStream = new System.Net.Security.SslStream(clientStream, false, new System.Net.Security.RemoteCertificateValidationCallback(VerifyCertificate), null, System.Net.Security.EncryptionPolicy.RequireEncryption);
sStream.AuthenticateAsServer(serverCert, true, System.Security.Authentication.SslProtocols.Tls12, false);
var fiftyFive = sStream.ReadByte();
if (fiftyFive != 55)
throw new Exception($"Expected 55, got {fiftyFive}");
}
static void Main(string[] args)
{
if (!System.IO.File.Exists("CA.pfx"))
MakeCertificates();
CRNG.Dispose();
var t1 = new System.Threading.Thread(RunServer);
t1.Start();
//TODO: wait for server to start before starting client
System.Threading.Thread.Sleep(1000);
var t2 = new System.Threading.Thread(RunClient);
t2.Start();
t1.Join();
t2.Join();
}
static void MakeCertificates()
{
MakeCA();
MakeCert("127.0.0.1");
MakeCert("Client");
}
static void MakeCA()
{
var ecdsa = System.Security.Cryptography.ECDsa.Create(); // generate asymmetric key pair
var req = new System.Security.Cryptography.X509Certificates.CertificateRequest($"cn=Certificate Authority", ecdsa, System.Security.Cryptography.HashAlgorithmName.SHA256);
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true));
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension(req.PublicKey, false));
var cert = req.CreateSelfSigned(System.DateTimeOffset.Now, System.DateTimeOffset.Now.AddYears(1000));
System.IO.File.WriteAllBytes("CA.pfx", cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx));
System.IO.File.WriteAllText("CA.crt",
"-----BEGIN CERTIFICATE-----\r\n"
+ System.Convert.ToBase64String(cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert), System.Base64FormattingOptions.InsertLineBreaks)
+ "\r\n-----END CERTIFICATE-----");
}
static System.Security.Cryptography.RandomNumberGenerator CRNG = System.Security.Cryptography.RandomNumberGenerator.Create();
static void MakeCert(string cn)
{
var ecdsa = System.Security.Cryptography.ECDsa.Create(); // generate asymmetric key pair
var ca = new System.Security.Cryptography.X509Certificates.X509Certificate2("CA.pfx");
var req = new System.Security.Cryptography.X509Certificates.CertificateRequest($"cn={cn}", ecdsa, System.Security.Cryptography.HashAlgorithmName.SHA256);
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(false, false, 0, false));
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509KeyUsageExtension(System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.DigitalSignature | System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.NonRepudiation, false));
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension(req.PublicKey, false));
req.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension(new System.Security.Cryptography.OidCollection { new System.Security.Cryptography.Oid("1.3.6.1.5.5.7.3.8") }, true));
var serial = new byte[20];
CRNG.GetBytes(serial);
var cert = req.Create(ca, System.DateTime.Now, System.DateTime.Now.AddYears(500), serial);
cert = System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions.CopyWithPrivateKey(cert, ecdsa);
System.IO.File.WriteAllBytes($"{cn}.pfx", cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx));
System.IO.File.WriteAllText($"{cn}.crt",
"-----BEGIN CERTIFICATE-----\r\n"
+ System.Convert.ToBase64String(cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert), System.Base64FormattingOptions.InsertLineBreaks)
+ "\r\n-----END CERTIFICATE-----");
}
}
}

I'm not sure if it's just a bug on Windows, but I switched to RSA instead of ECDSA and now it works on both Linux and Windows.
Key generation is a lot slower (not sure about processing overhead from data transfer yet), so if anyone has a solution for using ECDSA on Windows I'd prefer that.

Related

MQTTnet TLS 1.2 Encrypted Server

I'm trying to create TLS 1.2-encrypted broker and clients with MQTTnet (let's say on port 2000). Below is my attempt:
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Server;
using System.Security.Authentication;
MqttFactory factory = new MqttFactory();
MqttServerOptionsBuilder serverOptions = new MqttServerOptionsBuilder()
.WithEncryptedEndpoint()
.WithEncryptedEndpointPort(2000)
.WithEncryptionSslProtocol(SslProtocols.Tls12)
.WithoutDefaultEndpoint();
MqttServer mqttServer = factory.CreateMqttServer(serverOptions.Build());
mqttServer.StartAsync();
MqttClientOptionsBuilder clientOptions = new MqttClientOptionsBuilder()
.WithClientId("myClient")
.WithTcpServer("localhost", 2000)
.WithTls(new MqttClientOptionsBuilderTlsParameters()
{
UseTls = true,
SslProtocol = SslProtocols.Tls12,
CertificateValidationHandler = x => { return true; }
});
MQTTnet.Client.MqttClient mqttClient = factory.CreateMqttClient() as MQTTnet.Client.MqttClient;
while (!mqttClient.IsConnected)
{
mqttClient.ConnectAsync(clientOptions.Build()).GetAwaiter();
Thread.Sleep(1000);
}
Console.WriteLine("Connected");
Console.ReadLine();
The client I created doesn't connect to the broker. I believe the problem comes from the server side (if not both), as nothing is connected on port 2000 when I check with netstat.
What did I miss?
Here's the code that works for me. Basically after awaiting the server and adding a X509 certificate the server now allows clients with the same certificate to connect.
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Server;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
X509Store store = new X509Store(StoreLocation.CurrentUser);
X509Certificate2 certificate;
try
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection = store.Certificates;
X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
certificate = currentCerts[0];
}
finally
{
store.Close();
}
MqttFactory factory = new MqttFactory();
MqttServerOptionsBuilder serverOptions = new MqttServerOptionsBuilder()
.WithEncryptedEndpoint()
.WithEncryptedEndpointPort(2000)
.WithEncryptionCertificate(certificate)
.WithRemoteCertificateValidationCallback( (obj, cert, chain, ssl) => { return true; } )
.WithEncryptionSslProtocol(SslProtocols.Tls12)
.WithoutDefaultEndpoint();
MqttServer mqttServer = factory.CreateMqttServer(serverOptions.Build());
await mqttServer.StartAsync();
MqttClientOptionsBuilder clientOptions = new MqttClientOptionsBuilder()
.WithClientId("myClient")
.WithTcpServer("localhost", 2000)
.WithTls(new MqttClientOptionsBuilderTlsParameters()
{
UseTls = true,
SslProtocol = SslProtocols.Tls12,
CertificateValidationHandler = x => { return true; }
});
MQTTnet.Client.MqttClient mqttClient = factory.CreateMqttClient() as MQTTnet.Client.MqttClient;
while (!mqttClient.IsConnected)
{
mqttClient.ConnectAsync(clientOptions.Build()).GetAwaiter();
Thread.Sleep(1000);
}
Console.WriteLine("Connected");
Console.ReadLine();

My gRPC-Test Project just work on localhost

i have the following problem.
I created a gRpc Server(Console App .Net 4.7.2 - i cant do Net Core on Server Side because of Crystal Reports :() and a Client(WPF App .Net Core 3.1) and i can run it as long as Server and Client are on my machine (Windows 10). As far es i take my Server to another machine (windows Server 2016), it does not work anymore.
this is the RPC Exception:
Status(StatusCode="Unavailable", Detail="failed to connect to all
addresses",
DebugException="Grpc.Core.Internal.CoreErrorDetailException:
{"created":"#1595508082.170000000","description":"Failed to pick
subchannel","file":"T:\src\github\grpc\workspace_csharp_ext_windows_x64\src\core\ext\filters\client_channel\client_channel.cc","file_line":3948,"referenced_errors":[{"created":"#1595508082.170000000","description":"failed
to connect to all
addresses","file":"T:\src\github\grpc\workspace_csharp_ext_windows_x64\src\core\ext\filters\client_channel\lb_policy\pick_first\pick_first.cc","file_line":394,"grpc_status":14}]}")
i tried all variations. Here is my lastcode that works on localhost:
Server:
static void Main(string[] args)
{
var cacert = File.ReadAllText(#"root.crt");
var servercert = File.ReadAllText(#"server.crt");
var serverkey = File.ReadAllText(#"server.key");
var keypair = new KeyCertificatePair(servercert, serverkey);
var sslCredentials = new SslServerCredentials(new List<KeyCertificatePair>() { keypair }, cacert, false);
// Build a server
var server = new Server
{
Services = { ReportService.BindService(new KKarteReportService()) },
Ports = { new ServerPort(Host, Port, sslCredentials) }
};
// Start server
server.Start();
Console.WriteLine("KKarteReport Server listening on port " + Port);
Console.WriteLine("Press any key to stop the server...");
Console.ReadKey();
server.ShutdownAsync().Wait();
}
Client
var cacert = File.ReadAllText(#"root.crt");
var clientcert = File.ReadAllText(#"client.crt");
var clientkey = File.ReadAllText(#"client.key");
var ssl = new SslCredentials(cacert, new KeyCertificatePair(clientcert, clientkey));
var options = new List<ChannelOption> { new ChannelOption(ChannelOptions.SslTargetNameOverride, "MyServerHost") };
var channel = new Channel("12.20.18.11", 5001, ssl, options);
//var channel = new Channel("localhost", 5001, ssl, options);
//var channel = new Channel(url, ChannelCredentials.Insecure);
var client = new ReportService.ReportServiceClient(channel);
using var streamingCall = client.CreateAusschreibung(request);
await using var ms = new MemoryStream();
while (await streamingCall.ResponseStream.MoveNext())
{
ms.Write(streamingCall.ResponseStream.Current.FileChunk.ToByteArray());
}
What do i miss?
What does the ‘Host‘ variable contain on the server side? The issue might be a incorrect address binding, which prevents the service from being reachable from IP addresses other than localhost (127.0.0.1). Try entering 0.0.0.0 there.

What Inputs to provide to see a successful run of secure SslStream program

I am trying to gain some traction on secure socket implementation in c#. In my pursuit, I got a feeling about how TLS handshake works by going through Wikipedia and other sources.
I also thought to review some sample code and stumbled on the following https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream?redirectedfrom=MSDN&view=netframework-4.7.2
There are two programs(console applications) basically one for client and other for the server, to be able to run the program Main method requires to be fed with some arguments i.e. MachineName, and ServerCertificateName.
Please suggest how to get a hold of a certificate to be able to fulfill the certificate name field.
Here is simple example with 2 threads. You can separate these threads between 2 applications.
static void Main(string[] args)
{
var serverThread = new Thread(() =>
{
var server = new TcpListener(IPAddress.Any, 12345);
server.Start();
var client = server.AcceptTcpClient();
var ssl = new SslStream(client.GetStream());
// certificate file with private key and password
var cert = new X509Certificate2(#"rsa-4096.pfx", "hh87$-Jqo");
ssl.AuthenticateAsServer(cert);
ssl.Write(Encoding.ASCII.GetBytes("Hello world"), 0, 11);
ssl.Flush();
ssl.Close();
server.Stop();
});
var clientThread = new Thread(() =>
{
var client = new TcpClient();
client.Connect(IPAddress.Loopback, 12345);
// last parameter disables certificate validation
var ssl = new SslStream(client.GetStream(), false, (a, b, c, d) => true);
ssl.AuthenticateAsClient("localhost");
using (var sr = new StreamReader(ssl, Encoding.ASCII))
{
string recivedText = sr.ReadToEnd();
Console.WriteLine(recivedText);
}
});
serverThread.Start();
clientThread.Start();
serverThread.Join();
clientThread.Join();
}
You can download example certificate here: https://github.com/Zergatul/ZergatulLib/blob/master/ConsoleTest/rsa-4096.pfx

.NET Core 2.1 Apple Push Notifications

I have to send push notifications to specific iOS devices with my .Net Core WebAPI that will be executed on a Windows 2008 Server R2. The server itself should not be the problem because it is working with a node.js library. But I want it to work with an WepAPI in ASP .Net Core 2.1 which is self hosted with the inbuilt Kestrel Server. Maybe you've got an idea how to solve this problem.
My Code:
// This will encode the jason web token apns needs for the authorization
// get the base64 private key of the .p8 file from apple
string p8File = System.IO.File.ReadAllText(Settings.Apn.PrivateKey);
p8File = p8File.Replace("-----BEGIN PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace("-----END PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace(" ", string.Empty);
byte[] keyData = Convert.FromBase64String(p8File);
ECDsa key = new ECDsaCng(CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob));
ECDsaSecurityKey securityKey = new ECDsaSecurityKey(key) { KeyId = Settings.Apn.KeyId };
SigningCredentials credentials = new SigningCredentials(securityKey, "ES256");
SecurityTokenDescriptor descriptor =
new SecurityTokenDescriptor
{
IssuedAt = DateTime.Now,
Issuer = Settings.Apn.TeamId,
SigningCredentials = credentials
};
JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
string encodedToken = jwtHandler.CreateEncodedJwt(descriptor);
this.log?.LogInformation($"Created JWT: {encodedToken}");
// The hostname is: https://api.development.push.apple.com:443
HttpClient client = new HttpClient { BaseAddress = new Uri(Settings.Apn.Hostname) };
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
this.log?.LogInformation("Initialized new HttpClient.");
// payload content for the apns
JObject payloadData = new JObject
{
new JProperty("alert", data.Message),
new JProperty("badge", 2),
new JProperty("sound", "default")
};
JObject payload = new JObject
{
new JProperty("aps", payloadData)
};
this.log?.LogInformation($"Setup payload: {payload}");
// HttpRequestMessage that should be send
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
$"{Settings.Apn.Hostname}/3/device/{data.DeviceId}")
{
Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json")
};
this.log?.LogInformation("Setup HttpRequestMessage.");
// Setup the header
request.Headers.Add("Authorization", $"Bearer {encodedToken}");
request.Headers.Add("apns-id", Guid.NewGuid().ToString());
request.Headers.Add("apns-expiration", DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture));
request.Headers.Add("apns-priority", "10");
request.Headers.Add("apns-topic", "de.gefasoft-engineering.FabChat");
// Debug logging
this.log.LogDebug(request.ToString());
this.log.LogDebug(await request.Content.ReadAsStringAsync());
this.log.LogDebug(request.RequestUri.Host + request.RequestUri.Port);
// Send request
var result = await client.SendAsync(request);
this.log?.LogInformation("Sent request.");
this.log?.LogInformation(await result.Content.ReadAsStringAsync());
I always get following Exception thrown:
System.Net.Http.HttpRequestException: The SSL connection could not be
established, see inner exception. --->
System.Security.Authentication.AuthenticationException: Authentication
failed, see inner exception. --->
System.ComponentModel.Win32Exception: The message received was
unexpected or badly formatted --- End of inner exception stack
trace ---
Use CorePush lib
It's very lightweight. I use it across all my projects to send Firebase Android/WebPush and Apple iOS push notifications. Useful links:
NuGet package
Documentation
The interface is very simple and minimalistic:
Send APN message:
var apn = new ApnSender(settings, httpClient);
await apn.SendAsync(notification, deviceToken);
It can also send Android FCM message if needed:
var fcm = new FcmSender(settings, httpClient);
await fcm.SendAsync(deviceToken, notification);
can you try adding version information to your request after the apns-topic line as below? It ran to completion and I got a "bad device token" error for the first time after adding the following line.
request.Version = new Version(2, 0);
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
I saw the version setting command at the post below.
How to implement apple token based push notifications (using p8 file) in C#?
I've already commented on the answer from #civilator. But I think, that some people read over it, so I'm posting it again.
This is the code that worked for me. Sorry for the late answer!
private readonly string hostname = "gateway.sandbox.push.apple.com";
private readonly int port = 2195;
public async Task<RestResult<JObject>> SendPushNotification(string deviceToken, string message)
{
this.log?.LogInformation("Trying to send push notification.");
X509Certificate2Collection certificatesCollection;
// Setup and read the certificate
// NOTE: You should get the certificate from your apple developer account.
try
{
string certificatePath = Settings.Apn.Certificate;
X509Certificate2 clientCertificate = new X509Certificate2(
File.ReadAllBytes(certificatePath),
Settings.Apn.Password);
certificatesCollection = new X509Certificate2Collection(clientCertificate);
this.log?.LogInformation("Setup certificates.");
}
catch (Exception e)
{
this.log?.LogError(e.ToString());
return new RestResult<JObject> { Result = "exception", Message = "Failed to setup certificates." };
}
// Setup a tcp connection to the apns
TcpClient client = new TcpClient(AddressFamily.InterNetwork);
this.log?.LogInformation("Created new TcpClient.");
try
{
IPHostEntry host = Dns.GetHostEntry(this.hostname);
await client.ConnectAsync(host.AddressList[0], this.port);
this.log?.LogInformation($"Opened connection to {this.hostname}:{this.port}.");
}
catch (Exception e)
{
this.log?.LogError("Failed to open tcp connection to the apns.");
this.log?.LogError(e.ToString());
}
// Validate the Certificate you get from the APN (for more information read the documentation:
// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1).
SslStream sslStream = new SslStream(
client.GetStream(),
false,
new RemoteCertificateValidationCallback(this.ValidateServerCertificate),
null);
try
{
await sslStream.AuthenticateAsClientAsync(this.hostname, certificatesCollection, SslProtocols.Tls, false);
MemoryStream memoryStream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(memoryStream);
writer.Write((byte)0);
writer.Write((byte)0);
writer.Write((byte)32);
writer.Write(HexStringToByteArray(deviceToken.ToUpper()));
// Creating an payload object to send key values to the apns
JObject aps = new JObject
{
new JProperty("alert", message),
new JProperty("badge", 0),
new JProperty("sound", "default")
};
JObject payload = new JObject
{
new JProperty("aps", aps)
};
string payloadString = JsonConvert.SerializeObject(payload);
writer.Write((byte)0);
writer.Write((byte)payloadString.Length);
byte[] b1 = System.Text.Encoding.UTF8.GetBytes(payloadString);
writer.Write(b1);
writer.Flush();
byte[] array = memoryStream.ToArray();
sslStream.Write(array);
sslStream.Flush();
client.Dispose();
}
catch (AuthenticationException ex)
{
this.log?.LogError(ex.ToString());
client.Dispose();
return new RestResult<JObject> { Result = "exception", Message = "Authentication Exception." };
}
catch (Exception e)
{
this.log?.LogError(e.ToString());
client.Dispose();
return new RestResult<JObject> { Result = "exception", Message = "Exception was thrown." };
}
this.log?.LogInformation("Notification sent.");
return new RestResult<JObject> { Result = "success", Message = "Notification sent. Check your device." };
}
#region Helper methods
private static byte[] HexStringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
// The following method is invoked by the RemoteCertificateValidationDelegate.
private bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
this.log?.LogInformation("Server Certificate validated.");
return true;
}
this.log?.LogError($"Server Certificate error: {sslPolicyErrors}");
// Do not allow this client to communicate with unauthenticated servers.
return false;
}
#endregion

Add client certificate to .NET Core HttpClient

I was playing around with .NET Core and building an API that utilizes payment APIs. There's a client certificate that needs to be added to the request for two-way SSL authentication.
How can I achieve this in .NET Core using HttpClient?
I have looked at various articles and found that HttpClientHandler doesn't provide any option to add client certificates.
I ran a fresh install for my platform (Linux Mint 17.3) following these steps: .NET Tutorial - Hello World in 5 minutes. I created a new console application targeting the netcoreapp1.0 framework, was able to submit a client certificate; however, I did receive "SSL connect error" (CURLE_SSL_CONNECT_ERROR 35) while testing, even though I used a valid certificate. My error could be specific to my libcurl.
I ran the exact same thing on Windows 7 and it worked exactly as needed.
// using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();
I have a similar project where I communicate between services as well as between mobile and desktop with a service.
We use the Authenticode certificate from the EXE file to ensure that it's our binaries that are doing the requests.
On the requesting side (over simplified for the post).
Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
{
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(cert2);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
var myModel = new Dictionary<string, string>
{
{ "property1","value" },
{ "property2","value" },
};
using (var content = new FormUrlEncodedContent(myModel))
using (var _client = new HttpClient(_clientHandler))
using (HttpResponseMessage response = _client.PostAsync($"{url}/{controler}/{action}", content).Result)
{
response.EnsureSuccessStatusCode();
string jsonString = response.Content.ReadAsStringAsync().Result;
var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
}
}
I then use the following code on the action that gets the request:
X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
{
Response.StatusCode = 404;
return null;
}
We rather provide a 404 than a 500 as we like those that are trying URLs to get a bad request rather then let them know that they are "on the right track"
In .NET Core, the way to get the certificate is no longer by going over Module. The modern way that might work for you is:
private static X509Certificate2? Signer()
{
using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
if (cert is null)
return null;
return new X509Certificate2(cert);
}
After a lot of testing with this issue I ended up with this.
Using SSL, I created a pfx file from the certificate and key.
Create a HttpClient as follows:
_httpClient = new(new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12,
ClientCertificates = { new X509Certificate2(#"C:\kambiDev.pfx") }
});
I'm not using .NET for my client, but server side it can be configured simply via IIS by deploying my ASP.NET Core website behind IIS, configuring IIS for HTTPS + client certificates:
IIS client certificate setting:
Then you can get it simply in the code:
var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();
if(clientCertificate!=null)
return new ContentResult() { Content = clientCertificate.Subject };
It's working fine for me, but I'm using curl or chrome as clients, not the .NET ones. During the HTTPS handshake, the client gets a request from the server to provide a certificate and send it to the server.
If you are using a .NET Core client, it can't have platform-specific code and it would make sense if it couldn't connect itself to any OS specific certificates store, to extract it and send it to the server. If you were compiling against .NET 4.5.x then it seems easy:
Using HttpClient with SSL/TLS-based client side authentication
It's like when you compile curl. If you want to be able to connect it to the Windows certificates store, you have to compile it against some specific Windows library.
Can be used for both .NET Core 2.0< and .NET Framework 4.7.1<:
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1
Make all configuration in Main() like this:
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
string env="", sbj="", crtf = "";
try
{
var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());
var environment = env = whb.GetSetting("environment");
var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
var certificate = CertificateHelper.GetServiceCertificate(subjectName);
crtf = certificate != null ? certificate.Subject : "It will after the certification";
if (certificate == null) // present apies even without server certificate but dont give permission on authorization
{
var host = whb
.ConfigureKestrel(_ => { })
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
})
.Build();
host.Run();
}
else
{
var host = whb
.ConfigureKestrel(options =>
{
options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
{
var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.AllowCertificate,
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
ServerCertificate = certificate
};
listenOptions.UseHttps(httpsConnectionAdapterOptions);
});
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls("https://*:443")
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
})
.Build();
host.Run();
}
Log.Logger.Information("Information: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf);
}
catch(Exception ex)
{
Log.Logger.Error("Main handled an exception: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf +
" Exception Detail = " + ex.Message);
}
}
Configure file startup.cs like this:
#region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
})
.AddCertificateAuthentication(certOptions =>
{
var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
});
services.AddAuthorization(options =>
{
options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
});
#endregion
The certificate helper
public class CertificateHelper
{
protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
{
using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
{
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(
X509FindType.FindBySubjectDistinguishedName, subjectName, true);
X509Certificate2 certificate = null;
if (certCollection.Count > 0)
{
certificate = certCollection[0];
}
return certificate;
}
}
protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.{environment}.json", optional: false);
var configuration = builder.Build();
return configuration["ServerCertificateSubject"];
}
}
If you look at the .NET Standard reference for the HttpClientHandler class, you can see that the ClientCertificates property exists, but is hidden due to the use of EditorBrowsableState.Never. This prevents IntelliSense from showing it, but will still work in code that uses it.
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get; }
I thought the best answer for this was provided here.
By utilizing the X-ARR-ClientCert header you can provide the certificate information.
An adapted solution is here:
X509Certificate2 certificate;
var handler = new HttpClientHandler {
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) => {
return true;
};
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);
return keyResponse;
}
And in your .net core server's Startup routine:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCertificateForwarding(options => {
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) => {
X509Certificate2 clientCertificate = null;
try
{
if (!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = ConvertHexToBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
}
catch (Exception)
{
// invalid certificate
}
return clientCertificate;
};
});
}

Categories