ECDSA-signed JWT validation failures between .NET and PHP

Why does a ECDSA-signed JWT in .NET fail validation in PHP when using OpenSSL, even with matching keys and algorithms?

  • In .NET using System.Security.Cryptography and System.IdentityModel.Tokens.Jwt packages.
  • In PHP using OpenSSL extension and Firebase's JWT library.
  • Algo: ECDSA(ES256)
  • Public key format: PEM

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 FirebaseJWTJWT;
use FirebaseJWTKey;

$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:

  • ec-private-key.pem (private)
  • ec-public-key.pem (public, for JWT validation).

Any idea why this happens?