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:
<?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:
<?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-42ECDSA: 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:
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:
{
"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.
Related
- Verifying with JWKS — validate third-party tokens & rotate keys
- Algorithms — the full supported-algorithm matrix
- API reference —
PrivateKey,PublicKey,JWK