Why does a ECDSA-signed JWT in .NET fail validation in PHP when using OpenSSL, even with matching keys and algorithms?
System.Security.Cryptography
and System.IdentityModel.Tokens.Jwt
packages.OpenSSL
extension and Firebase's JWT
library.My example .NET code to generate JWT:
// ECDSA-signed JWT
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
var privateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); // ES256
var securityKey = new ECDsaSecurityKey(privateKey) { KeyId = "1" };
var credentials = new SigningCredentials(securityKey, "ES256");
var token = new JwtSecurityToken(
issuer: "https://example.com",
audience: "https://example.com",
claims: new[] { new Claim("test", "value") },
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
Console.WriteLine("JWT (will fail in PHP): " + jwt);
Console.Read();
My example PHP code to consume JWT:
<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$jwt = '<JWT STRING HERE>'; // From .NET
$publicKey = openssl_pkey_get_public('path/to/public_key.pem'); // PEM-formatted public key.
try
{
$decoded = JWT::decode($jwt, new Key($publicKey, 'ES256'));
print_r($decoded);
}
catch (Exception $e)
{
echo 'Validation error: ' . $e->getMessage();
}
PHP output:
Validation error: OpenSSL error: error:1C800064:Provider routines::bad signature
My instructions to create EXTRA ECDSA PEM formatted public-key for JWT:
# Generate private-key.
openssl ecparam -name prime256v1 -genkey -noout -out ec-private-key.pem
# Extract public-key from private-key.
openssl ec -in ec-private-key.pem -pubout -out ec-public-key.pem
Output files:
Any idea why this happens?
I would generate the keypair with OpenSSL using your commands and then load them like this:
var privateKey = ECDsa.Create();
privateKey.ImportFromPem(File.ReadAllText("private-key.pem"));
Then, to prove that the .NET code is fine, past both the JWT and the public key into the jwt.io utility to prove that the JWT is OK.
This was my example JWT and public key so maybe try using these values:
eyJhbGciOiJFUzI1NiIsImtpZCI6IjEiLCJ0eXAiOiJKV1QifQ.eyJ0ZXN0IjoidmFsdWUiLCJleHAiOjE3NDY3MjgzMjQsImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIn0.hTnl1kHaTQ42lr67pkHLXjtcKrYotmveAc3WBAFoXSq-W2TzZA8s13T9cLNxo5GJzvguNxI1Yfto-EOAXvhwqg
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvXGdGX5f3zXCzOUNL5uGHLSOi1NA
E3VL4vAOBTnAX051u553WGcU33qrrLsoqDYquaokx+zwllBr9sKIJiOQ5w==
-----END PUBLIC KEY-----
On the PHP side of things, I created a file called decode.php
with this content. I had to change the code that loaded the public key, though I encountered a different error to you.
```php
<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$jwt = 'eyJhbGciOiJ ...';
$publicKey = openssl_pkey_get_public(file_get_contents('./public-key.pem'));
try
{
$decoded = JWT::decode($jwt, new Key($publicKey, 'ES256'));
print_r($decoded);
}
catch (Exception $e)
{
echo 'Validation error: ' . $e->getMessage();
}
Decoding then succeeded with this output:
stdClass Object
(
[test] => value
[exp] => 1746728324
[iss] => https://example.com
[aud] => https://example.com
)
Perhaps it is something environmental. I actually ran from a Docker container built like this. I got a shell to the container and ran php ./decode.php
.
FROM php:8.1-cli
RUN apt-get update && apt-get install -y \
zlib1g-dev \
libzip-dev \
unzip
RUN docker-php-ext-install zip
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN composer self-update
WORKDIR /usr/myapp
COPY decode.php /usr/myapp
COPY public-key.pem /usr/myapp
RUN composer require firebase/php-jwt
CMD ["/bin/sleep", "infinity"]
So ES256 should work in both tech stacks since that algorithm is pretty well supported these days. Follow an elimination approach and use standard tools to verify correctness, so that you can narrow down your issue.