Add client certificate to .NET Core HttpClient - c#

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;
};
});
}

Related

Is there a way to use an HTTPS proxy in .NET 6?

I need to check if certain HTTPS proxies are working. To do so, I originally thought of using the following code:
var proxy = new WebProxy($"https://proxy-server:port");
var handler = new HttpClientHandler { Proxy = proxy };
using (var client = new HttpClient(handler))
{
client.BaseAddress = new Uri("https://base-address");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
try
{
var content = client.GetStringAsync(filepath).Result;
Console.WriteLine(content);
}
catch (Exception e)
{
Console.WriteLine("Failed. Reason: " + e);
}
}
If I try to access the base-address without the proxy, it works fine. Using the proxy I get an error saying I can only use HTTP or SOCKS proxies. The problem is, I wasn't able to find an alternate solution that supports HTTPS proxies.
You're getting the error because the WebProxy class in .NET does not support HTTPS proxies. However, you can use the HttpClientHandler's SslOptions property to configure the SSL/TLS settings.
var proxy = new WebProxy($"http://proxy-server:port");
var handler = new HttpClientHandler
{
Proxy = proxy,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
}
};
using (var client = new HttpClient(handler))
{
client.BaseAddress = new Uri("https://base-address");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
try
{
var content = client.GetStringAsync(filepath).Result;
Console.WriteLine(content);
}
catch (Exception e)
{
Console.WriteLine("Failed. Reason: " + e);
}
}
Create a new instance of HttpClientHandler and set its Proxy property to a new WebProxy that represents the HTTP proxy server.
Set the SslOptions property to a new instance of SslClientAuthenticationOptions and configure the RemoteCertificateValidationCallback to return true, indicating that you want to accept any server certificate.
Create a new instance of HttpClient and pass the handler to its constructor.
Finally, use the client to send an HTTPS GET request to the base-address website.

Accessing HttpClientHandler.Credentials on the server?

I'm trying to implement basic authentication on a Web API server and a client that calls the API.
This is how I'm configuring my HttpClient object in my client code (note how I'm setting the credentials).
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
})
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
{
return new HttpClientHandler()
{
Credentials = new NetworkCredential(settings.Username, settings.Password),
};
});
However, this results in no Authorization header being received on the server end.
My question is, what does setting the credentials this way actually do? Where are those credentials placed?
And is it possible to write my server so that it handles credentials configured this way?
You can add Authorization header to your client-side code as follows:
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
var authenticationBytes = Encoding.ASCII.GetBytes($"{settings.Username}:{settings.Password}");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authenticationBytes));
});
You can use AuthenticationHandler to check Authorization header on the server side. Here is an example of how to implement Basic Authentication using AuthenticationHandler.
The PreAuthenticate should send the authentication header.
services.AddHttpClient<TrackAndTraceClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(settings.BaseUrl);
httpClient.Timeout = TimeSpan.FromMinutes(5);
})
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>
{
return new HttpClientHandler()
{
PreAuthenticate = true,
Credentials = new NetworkCredential(settings.Username,
settings.Password)
};
});
If that does not work, it may be due to the Credentials property in the HttpClientHandler class having the[UnsupportedOSPlatform("browser")] attribute (as of .NET 5) -- note so does the PreAuthenticate property. In addition, the HttpClientHandler class also contains the following definition:
The default message handler used by HttpClient in .NET Framework and .NET Core 2.0 and earlier.
I see that your post has the tag .NET 6, so that might could also be a factor on why it's not sending. If I was you, I would use a middleware to rece9ve the authentication header from the request and use the solution #Burhan Savci posted for sending it.
Assuming your backend is a .NET Core API similar:
public class BasicAuthMiddleware
{
private readonly RequestDelegate _next;
public BasicAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
string authHeader =
httpContext.Request.Headers["Authorization"];
if (authHeader != null)
{
string auth = authHeader.Split(new char[] { ' ' })[1];
Encoding encoding = Encoding.GetEncoding("UTF-8");
var usernameAndPassword =
encoding.GetString(Convert.FromBase64String(auth));
string username = usernameAndPassword
.Split(new char[] { ':' })[0];
string password = usernameAndPassword
.Split(new char[] { ':' })[1];
if (username == "abc" && password == "123")
{
await _next(httpContext);
}
else
{
httpContext.Response.StatusCode = 401;
return;
}
}
else
{
httpContext.Response.StatusCode = 401;
return;
}
}
}
And then in the configure method of your Startup class:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<BasicAuthMiddleware>();
}

Windows/Linux dotnet core SslStream differences

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.

.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

Accessing a REST service which has a Client Certificate in Xamarin targeting Android

Does anyone know the best way to implement accessing a REST service which has a Client Certificate in Xamarin targeting Android?. I'm using .NET Standard 2.0 project for the shared code.
I've tried WebRequestHandler to add the certificate, but mono does not seem to support this and the application won't run. I have also tried HttpClientHandler.
Code snippet below :
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
X509Certificate2 certificate = App.CertificateFile;
var handler = new WebRequestHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual
};
handler.ClientCertificates.Add(certificate);
var client = new HttpClient(handler);
There is a similar question posted here Using custom SSL client certificates System.Net.HttpClient on Mono
but was a while ago so hoping things have improved. My code works correctly from a .Standard 2.0 project if called via a console app, but the same code does not work on the device.
You can try AndroidClientHandler and Programatically using androidClientHandler:
AndroidClientHandler clientHandler = new AndroidClientHandler();
Java.Security.Cert.X509Certificate cert = null;
try
{
CertificateFactory factory = CertificateFactory.GetInstance("X.509");
using (var stream = Application.Context.Assets.Open("MyCert.pfx"))
{
cert = (Java.Security.Cert.X509Certificate)factory.GenerateCertificate(stream);
}
} catch (Exception e)
{
System.Console.WriteLine(e.Message);
}
if (clientHandler.TrustedCerts != null)
{
clientHandler.TrustedCerts.Add(cert);
}
else
{
clientHandler.TrustedCerts = new List<Certificate>();
clientHandler.TrustedCerts.Add(cert);
}
HttpClient client = new HttpClient(clientHandler);
Update:
If the up codes doesn't work, you can try the Android Native implementation, which leverage the same thing as AndroidClientHandler, but are more flexible for use:
var keyStore = KeyStore.GetInstance("PKCS12");
string clientCertPassword = "password_of_certificate";
using (var stream = Application.Context.Assets.Open("cert.pfx"))
{
keyStore.Load(stream, clientCertPassword.ToCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.GetInstance("x509");
kmf.Init(keyStore, clientCertPassword.ToCharArray());
IKeyManager[] keyManagers = kmf.GetKeyManagers();
SSLContext sslContext = SSLContext.GetInstance("TLS");
sslContext.Init(keyManagers, null, null);
String result = null;
HttpURLConnection urlConnection = null;
HttpStatus lastResponseCode;
try
{
URL requestedUrl = new URL("https://10.106.92.42:444");
urlConnection = (HttpURLConnection)requestedUrl.OpenConnection();
if (urlConnection is HttpsURLConnection) {
((HttpsURLConnection)urlConnection).SSLSocketFactory = sslContext.SocketFactory;
}
urlConnection.RequestMethod = "GET";
urlConnection.ConnectTimeout = 1500;
urlConnection.ReadTimeout = 1500;
lastResponseCode = urlConnection.ResponseCode;
result = ReadFully(urlConnection.InputStream);
string lastContentType = urlConnection.ContentType;
}
catch (Exception ex)
{
result = ex.ToString();
}
finally
{
if (urlConnection != null)
{
urlConnection.Disconnect();
}
}

Categories