I'm trying to read a PEM encoded private key (.p8 file) and use it to encrypt a JWT token to communicate with apple APNS servers. At first, I had the problem described here: #1918 (Not implemented on non-windows platforms).
The comments mention, using ECDsa.Create() since it is implemented, so I tried that. I implemented the following code, which works fine on Windows (10 x64). However, when I deploy to Ubuntu (ubuntu.18.04-x64), I get an error.
var ecPrivateKeyParameters =
(ECPrivateKeyParameters)new Org.BouncyCastle.OpenSsl.PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
// Convert the BouncyCastle key to a Native Key.
var msEcp = new ECParameters();
msEcp.Curve = ECCurve.NamedCurves.nistP256;
msEcp.Q.X = x;
msEcp.Q.Y = y;
msEcp.D = d;
return ECDsa.Create(msEcp);
I'm using BouncyCastle to read the PEM file, and then I manually convert the result to a ECDsa. This works perfectly fine in windows and I'm able to correctly encrypt a JWT token. However when I run under ubuntu.18.04-x64, I get the following error:
Unhandled Exception: Interop+Crypto+OpenSslCryptographicException: error:100B107B:elliptic curve routines:EC_KEY_check_key:invalid private key
at System.Security.Cryptography.ECOpenSsl.ImportParameters(ECParameters parameters)
at System.Security.Cryptography.ECDsa.Create(ECParameters parameters)
At first I thought this was a file permission error, but I have confirmed the PEM file is being read properly. However ECDsa.Create seems to fail due to some implementation difference on the two platforms. Is there something I'm doing wrong? Or if this is a bug, is there any workarounds?
Given that I have no idea what Bouncy Castle is doing, the best thing I can recommend:
Since the array lengths seem to be valid, my only real guess is a Big/Little-Endian encoding problem, but I might get more insight with data in front of me.
I have generated a new p8 file and attached the file in a .zip.
testkey.zip
Here are the hex dumps of the 32 byte arrays X,Y,D.
Q.X
6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
Q.Y
4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
D
9F9BD156374FB78F3D69EFF10DEF8C296EC4F03EACA42F4257130D0CE9316FCD
Let me know if there is anything else I can help with to help understand this issue
D = 9F9BD156374FB78F3D69EFF10DEF8C296EC4F03EACA42F4257130D0CE9316FCD (looks correct).
Q.X in the file says: C995C0C4DD52077EA5FFD4BBBD3F8D251ACCC19B55B3957B8F8F1D0B1DA70502
Q.Y in the file says: 741A1B88D26B12F57BC0D7D09FE9FC51FDD13747A2B972F16ED69950BCD79E82
In a moment of inspiration I searched the Q.X value. You're getting G.X from Bouncy Castle (G being the curve generator point), not Q.X. Sure enough, you're reading Parameters.G.
Windows must be ignoring Q when D is provided, and recomputing it. Sigh.
I stuck with similar problem a while ago. Workable solution was not to touch anything from Bouncy libraries... or completely use Bouncy
In my case one part was on hardware (smart card) so i implemented everything in Bouncy.NET at the end...
@furoraest Unfortunately I'm using other libraries that require native .NET key objects, and .NET does not support reading keys from PEM encoded files (as far as I know), which is why I needed BouncyCastle.
@bartonjs
Thanks for the insight. I think you are spot on. Windows must be ignoring my (incorrect) coordinates I set on Q, if D is set. Unix implementations simply fail.
For anyone else that arrives here, I was able to solve this by generating the public key with BouncyCastle to compute Q manually and then pass Q coordinates to the .NET ECParameters:
// Compute Public Key (Q)
Org.BouncyCastle.Math.EC.ECPoint q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();
// Convert the BouncyCastle key to a Native Key.
var msEcp = new ECParameters();
msEcp.Curve = ECCurve.NamedCurves.nistP256;
msEcp.Q.X = q.AffineXCoord.GetEncoded();
msEcp.Q.Y = q.AffineYCoord.GetEncoded();
msEcp.D = d;
return ECDsa.Create(msEcp);
I would like to see the Windows/Unix implementations more consistent (eg have the Unix implementation generate Q automatically) but I can understand this is probably quite a small use case.
.NET does not support reading keys from PEM encoded files (as far as I know)
netcoreapp30 has this functionality :smile: https://github.com/dotnet/corefx/issues/20414.
I would like to see the Windows/Unix implementations more consistent (eg have the Unix implementation generate Q automatically)
The Apple implementation doesn't have access to a point multiplier, only "import this PKCS8"; so the least common denominator is probably to make the Windows implementation reject the import of bad parameters, even if CNG allowed it (or see if there's a "make sure this makes sense" flag).
Moved the meat of this issue to https://github.com/dotnet/corefx/issues/33278.
Most helpful comment
@furoraest Unfortunately I'm using other libraries that require native .NET key objects, and .NET does not support reading keys from PEM encoded files (as far as I know), which is why I needed BouncyCastle.
@bartonjs
Thanks for the insight. I think you are spot on. Windows must be ignoring my (incorrect) coordinates I set on Q, if D is set. Unix implementations simply fail.
For anyone else that arrives here, I was able to solve this by generating the public key with BouncyCastle to compute Q manually and then pass Q coordinates to the .NET ECParameters:
I would like to see the Windows/Unix implementations more consistent (eg have the Unix implementation generate Q automatically) but I can understand this is probably quite a small use case.