I'm trying to send a PushNotification Message to a list of iOS device
.
My constraints are :
- code is inside a webapi controller
- web server is a Windows 2012 R2
- using asp.net / .net core (actually 3.1)
First, I decided to use p8 file instead of p12 (it seems Apple prefere it).
I crawled many SO questions, trying about 5-6 solutions, but i still get same results :
An unhandled exception occurred while processing the request. Win32Exception: Le message reçu était inattendu ou formaté de façon incorrecte. Unknown location
AuthenticationException: Authentication failed, see inner exception. System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
HttpRequestException: The SSL connection could not be established, see inner exception. System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
From french "Win32Exception: Le message reçu était inattendu ou formaté de façon incorrecte", in English would be "Win32Exception: The message received was unexpected or badly formatted"
To format the JWT, i've used the solution here from Bourne Koloh.
I've also tried PushSharp and CoreSharp with same error.
When I extract data from the p8 file, i have all the file without first and last line, and without linebreak.
When i use the Push Notifications Tester application, it works, the message is delivered to the device.
By the way, i've added the both p12 certificate (dev and prod) to the Windows 2012 R2 server I probably miss something important but still don't know what.
Since the message say it's a problem of authentication, I guess it's around the JWT and the the p8 file.
How I extract the p8 file data :
var data = System.IO.File.ReadAllText("AuthKey.p8");
var list = data.Split('\n').ToList();
var prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
var key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
Il also tried this way (with BouncyCastle ):
using (var reader = System.IO.File.OpenText("AuthKey.p8"))
{
var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
var key = ECDsaCng(EccKey.New(x, y, d));
}
And now how I build the JWT :
try #1 : using Jose package.
private string GetProviderToken(ECDsaCng key)
{
var epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970,1,1)).TotalSeconds;
var payload = new Dictionary<string, object>()
{
{ "iss", "THETEAMID" },
{ "iat", epochNow }
};
var extraHeaders = new Dictionary<string, object>()
{
{ "kid", "THEKEYID"}
};
return JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders);
}
var token = GetProviderToken(key);
try #2 :
private string CreateToken(ECDsa key, string keyID, string teamID)
{
var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
var credentials = new SigningCredentials(securityKey, "ES256");
var descriptor = new SecurityTokenDescriptor
{
IssuedAt = DateTime.Now,
Issuer = teamID,
SigningCredentials = credentials
};
descriptor.Expires = null;
descriptor.NotBefore = null;
var handler = new JwtSecurityTokenHandler();
var encodedToken = handler.CreateEncodedJwt(descriptor);
return encodedToken;
}
var token = CreateToken(key, "THEKEYID", "THETEAMID");
try #3
private string SignES256(string privateKey, string header, string payload)
{
CngKey key = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
using (ECDsaCng dsa = new ECDsaCng(key))
{
dsa.HashAlgorithm = CngAlgorithm.Sha256;
var unsignedJwtData = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(header)) + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(payload));
var signature = dsa.SignData(System.Text.Encoding.UTF8.GetBytes(unsignedJwtData));
return unsignedJwtData + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(signature);
}
}
var token = SignES256(prk, "{\"alg\":\"ES256\" ,\"kid\":\"THEKEYID\"}", "{ \"iss\": \"THETEAMID\",\"iat\":" + (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds + "\" }");
If I'm not using a library (CorePush, etc) for sending the http message, i do this way :
var url = string.Format("https://api.sandbox.push.apple.com/3/device/{0}", deviceToken);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
request.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
request.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
request.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
request.Headers.TryAddWithoutValidation("apns-topic", "com.company.project");
request.Content = new StringContent("{\"aps\":{\"alert\":\"Hello\"},\"yourCustomKey\":\"1\"}");
// also tried without yourcustomkey
request.Version = new Version(2, 0); // tried directly with System.Net.HttpVersion.Version20;
var handler = new HttpClientHandler();
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; // Tried with only Tls12
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
using (HttpClient client = new HttpClient(handler))
{
HttpResponseMessage resp = await client.SendAsync(request).ContinueWith(responseTask =>
{
return responseTask.Result; // line of error
});
if (resp != null)
{
string apnsResponseString = await resp.Content.ReadAsStringAsync();
handler.Dispose();
//ALL GOOD ....
return Ok(apnsResponseString);
}
handler.Dispose();
}