Pragmatism in the real world

Creating JWKS.json file in PHP

In order to verify a JWT created with an asymmetric key, the verifier needs to get the correct public key. One way to do is described in RFC7517 which describes the JSON Web Key format.

Within the header of the JWT there is a kid property which is the key ID which is then used to find the correct key within a list provided at the /.well-known/jwks.json endpoint.

The JWT header therefore looks something like this:

{
  "alg" : "RS256",
  "kid" : "6eaf334518784ff392c3123b41ae49f5",
  "typ" : "JWT"
}

And the jwks.json is structured something like this:

{
    "keys": [
        {
            "alg": "RS256",
            "kty": "RSA",
            "use": "sig",
            "kid": "6eaf334518784ff392c3123b41ae49f5",
            "n": "sj6R1AYPKISqYKFxmQMMFJSm583Jfn6ef51SpQPCe17SM10Ljp2YIte924U ...",
            "e": "AQAB"
        }
    ]
}

This is an interesting format as it doesn’t use the standard PEM format for the key, but rather stores it as a modulus (“n”) and exponent (“e”) as per RFC 7518 section 6.3:

6.3.1.1. “n” (Modulus) Parameter
The “n” (modulus) parameter contains the modulus value for the RSA
public key. It is represented as a Base64urlUInt-encoded value.
Note that implementers have found that some cryptographic libraries
prefix an extra zero-valued octet to the modulus representations they
return, for instance, returning 257 octets for a 2048-bit key, rather
than 256. Implementations using such libraries will need to take
care to omit the extra octet from the base64url-encoded
representation.
6.3.1.2. “e” (Exponent) Parameter
The “e” (exponent) parameter contains the exponent value for the RSA
public key. It is represented as a Base64urlUInt-encoded value.

Fortunately, we can use openssl to sort this all out for us:

// $keyString is a PEM encoded public key
$key = openssl_get_publickey($keyString);
$details = openssl_pkey_get_details($key);

Assuming $key is an instance of OpenSSLAsymmetricKey and $details is an array, then:

$modulus = $details['rsa']['n'];
$exponent = $details['rsa']['e'];

Putting this into a PSR-15 request handler that is passed an array of public keys, we can put together a jwks.json response:

<?php

declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Webmozart\Assert\Assert;

class JwksHandler implements RequestHandlerInterface
{
    /**
     * @param string[] $publicKeys
     */
    public function __construct(private readonly array $publicKeys)
    {
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $keys = [];

        foreach ($this->publicKeys as $keyString) {
            $key = openssl_get_publickey($keyString);
            Assert::isInstanceOf(\OpenSSLAsymmetricKey::class, 'Public key is not valid.');

            $details = openssl_pkey_get_details($key);
            Assert::isArray($details, 'Public key details are not valid.');

            $keys[] = [
                'kty' => 'RSA',
                'alg' => 'RS256',
                'use' => 'sig',
                'kid' => sha1($keyString),
                'n'   => strtr(rtrim(base64_encode($details['rsa']['n']), '='), '+/', '-_'),
                'e'   => strtr(rtrim(base64_encode($details['rsa']['e']), '='), '+/', '-_'),
            ];
        }

        return new JsonResponse(['keys' => $keys]);
    }
}

Note that we remove the base64 padding (= at the end) and also use the Base64Url modification where “+” is replaced with “-” and “/” with “_“.

The other properties in the JSON object are:

  • kty: Key Type – the cryptographic algorithm family used with the key
  • alg: Algorithm – the specific cryptographic algorithm used.
  • use: Use – the intended use of the public key. “sig” for signature, “enc” for encryption.
  • kid: Key ID – used to match a specific key

The verifier reads the jwks.json file and iterates over the list to find the one that matches the kid in the JWT header that they are trying to verify. When they find it, the can then convert the modulus exponent back into a public key and verify the JWT as per usual.

Thoughts? Leave a reply

Your email address will not be published. Required fields are marked *