Package · jwt

Asymmetric keys (RSA & ECDSA)

When the party that verifies a token shouldn’t be able to forge one, use asymmetric signing: a private key signs, and a freely shareable public key verifies. This guide covers RSA (RS*) and ECDSA (ES*), and the step worth taking once there is more than one key — a kid.

How the key is chosen

A kid (Key ID) is a pointer into a key set. RFC 7515 §4.1.4 makes it optional, and the rule is the same for every algorithm:

In the header Passed to decode() What happens
kid present a key under that id that key is used
kid present no such id refused — Key with 'kid'=… was not found
no kid exactly one key that key is used
no kid several keys refused — there is nothing to choose by

Why an unknown kid does not fall back to another key

If the header names a key, that key is used or none is. Trying keys until one fits would let a token influence which key verifies it, and that is the application’s decision, not the sender’s.

Several keys and no kid is refused for the same reason rather than “take the first”: verification would otherwise depend on array order.

A kid earns its place once there are several keys: rotation, several issuers, JWKS. With one key the third PrivateKey argument can be left out — the token stays valid for this library and for every other one.

RSA: sign and verify

Generate a pair (see Installation), then:

sign-rsa.php
<?php

use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;

$privateKey = openssl_pkey_get_private(file_get_contents('private.pem'));

$token = JWT::encode(
  new JwtPayload(['sub' => 'user-42', 'exp' => time() + 3600]),
  new PrivateKey($privateKey, 'RS256', 'rsa-key-1') // 3rd arg = kid
);

Verify with the public key, keyed by the same kid:

verify-rsa.php
<?php

use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\PublicKey;

$publicKey = openssl_pkey_get_public(file_get_contents('public.pem'));

$payload = JWT::decode($token, [
  'rsa-key-1' => new PublicKey($publicKey, 'RS256'),
]);

echo $payload->getClaim('sub'); // user-42

ECDSA: smaller keys, same flow

ECDSA gives you much shorter keys and signatures for equivalent security. The code is identical apart from the algorithm and curve:

php
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;

$privateKey = openssl_pkey_get_private(file_get_contents('ec-private.pem'));
$token = JWT::encode(
  new JwtPayload(['sub' => 'user-42', 'exp' => time() + 3600]),
  new PrivateKey($privateKey, 'ES256', 'ec-key-1')
);

$publicKey = openssl_pkey_get_public(file_get_contents('ec-public.pem'));
$payload = JWT::decode($token, [
  'ec-key-1' => new PublicKey($publicKey, 'ES256'),
]);

Curves map to algorithms

ES256 needs a P-256 key, ES384 a P-384 key, and ES512 a P-521 key. The library converts ECDSA’s raw signature to ASN.1 DER for OpenSSL automatically — see the Security model. Generate curves as shown in Installation.

Serve your public keys as a JWKS

Because the public key is safe to publish, the common pattern is to expose it as a JSON Web Key Set that verifiers fetch. You can build one entry from a parsed key, or just publish the standard fields your keys already have:

json
{
"keys": [
  {
    "kty": "RSA",
    "alg": "RS256",
    "kid": "rsa-key-1",
    "use": "sig",
    "n": "0vx7agoebGcQ...",
    "e": "AQAB"
  }
]
}

Consumers then verify without ever touching your private key — see Verifying with JWKS.

Choosing RSA vs ECDSA

RSA (RS*) ECDSA (ES*)
Key size (comparable security) 2048–4096 bit 256–521 bit
Signature size large small
Sign speed slower faster
Verify speed fast fast
Ubiquity universal very wide

Pick RSA for maximum interoperability with older systems; pick ECDSA for smaller tokens and faster signing. Both keep the private key private.