web-token/jwt-signature-algorithm-ecdsa
ECDSA signature algorithm support for the web-token/jwt PHP library, enabling JWT signing and verification with ES256, ES384, and ES512. Integrates with the JWT framework for standards-compliant elliptic-curve tokens.
web-token/jwt-signature-algorithm-ecdsa is a leaf package in the JWT Framework. It ships only the ES256, ES384, and ES512 algorithm classes — no builder, verifier, key management, or serializer. Installing it alone gives you almost nothing to call, so in practice you install an umbrella:
# Everything (recommended starting point)
composer require web-token/jwt-framework
# Or the v3+ consolidated library
composer require web-token/jwt-library
That said, this particular package is what actually pulls the ES* algorithms into the framework's AlgorithmManager, and it is loaded transitively by the umbrella packages — the low star count is misleading because nobody depends on a leaf package directly.
Where to look first, in this order:
src/ES256.php, src/ES384.php, src/ES512.php — each is ~15 lines: a name() method and a getECDSA() returning the curve name. Confirms nothing surprising is happening.src/ECDSA.php — the abstract base that actually calls openssl_sign() / openssl_verify() and does raw-signature packing. This is where all real behavior lives.composer.json — check which web-token/jwt-signature / jwt-core version it pairs with, because v1/v2/v3 namespaces differ.First use case — sign and verify a compact JWS:
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
$algorithms = new AlgorithmManager([new ES256()]);
// Generate a P-256 key once; persist the JWK JSON, not this call.
$jwk = \Jose\Component\KeyManagement\JWKFactory::createECKey('P-256');
$jws = (new JWSBuilder($algorithms))
->create()
->withPayload(json_encode(['sub' => '1234']))
->addSignature($jwk, ['alg' => 'ES256', 'kid' => 'k1'])
->build();
$token = (new CompactSerializer())->serialize($jws, 0);
// Verification side
$parsed = (new CompactSerializer())->deserialize($token);
$ok = (new JWSVerifier($algorithms))->verifyWithKey($parsed, $jwk, 0); // bool
That is the entire day-to-day surface: register the algorithm, feed a JWK, serialize.
Registry pattern, not direct instantiation. ES256 is never called directly in application code. You hand it to an AlgorithmManager, which is injected into JWSBuilder / JWSVerifier. In a Laravel app, bind a singleton in a service provider:
$this->app->singleton(AlgorithmManager::class, fn () => new AlgorithmManager([
new ES256(), new ES384(), new ES512(),
]));
Registering all three is cheap and lets you accept tokens signed with any of them.
JWK-only keys. The algorithms require a Jose\Component\Core\JWK (or JWKSet), not a PEM string or an OpenSSLAsymmetricKey. For signing, the JWK must include the private parameter d; for verification, x/y suffice. Load existing PEM material with JWKFactory::createFromKeyFile() or createFromCertificate() rather than hand-rolling the JWK array.
Symmetric asymmetry in your code paths. Signing needs the private key; verification needs the public key. Keep two factories — signingKey() returns the private JWK, verifyingKey() the public one — and never let the private one leak into the verification config, because a JWK with d set will silently verify fine and mask the mistake until you rotate.
Multi-algorithm acceptance with kid routing. For inbound tokens from third parties, deserialize the protected header, read kid, look up the matching JWK in a JWKSet, then verify. JWSVerifier::verifyWithKeySet() does this for you if the JWKSet is keyed correctly:
$verifier->verifyWithKeySet($jws, $jwkset, 0);
Custom algorithms extend ECDSA. If you need ES256K (secp256k1) or a non-standard curve, subclass the abstract class, implement name() and getECDSA(), and register it in the same manager. This is the sanctioned extension point and costs you one small class.
Integration tip — Laravel middleware. Wrap verification in middleware that attaches the verified claims to the request; do not verify inside controllers. The JWSVerifier is stateless and safe to reuse, so resolving it from the container per-request costs nothing.
Test strategy. Pin known-good tokens (RFC 7515 appendix A.3 has ES256 vectors) as fixtures for both sign and verify. When you upgrade the framework, a byte-diff on the compact serialization catches curve or encoding regressions immediately.
Curve-to-algorithm binding is strict. ES256 requires crv: P-256, ES384 → P-384, ES512 → P-521. The class names are misleading (ES512 is not P-512 — there is no such curve). Passing a P-384 key to ES256 throws at signing time, and the error comes from OpenSSL, not the framework, so it reads vaguely. Validate crv yourself if you build JWKs dynamically.
JOSE signatures are raw R||S, not DER. The package handles the conversion in ECDSA::sign(), but the moment you shell out to openssl or a hardware HSM that returns DER with a trailing byte-count (as OpenSSL's openssl_sign does), you must strip and normalize. Roughly half of all ECDSA interop bugs are this. Don't try to verify an externally-produced JOSE signature with openssl_verify() directly.
d must be present and correctly base64url-encoded. A JWK missing d produces a null/empty signature or a cryptic OpenSSL error rather than "private key required." If signing fails silently, dump the JWK's parameter names first.
The alg header must match a registered algorithm. A JWS built with ['alg' => 'ES256'] cannot be verified by an AlgorithmManager containing only ES384 — you get "algorithm not supported," not "wrong key." This bites when you trim the manager for performance.
ES512 / P-521 support depends on the OpenSSL build. Some distros and older PHP builds compile OpenSSL without secp521r1, and ES512 fails only at runtime. If your deployment targets Alpine or minimal Docker images, smoke-test ES512 in CI, or stick to ES256 (universally supported) and ES384.
ECDSA is randomized, so tokens are not reproducible. Snapshot tests that compare exact token strings will flake. Assert on verify-true, or freeze the RNG if you must (you can't with this package's OpenSSL path — use a test double implementing the SignatureAlgorithm interface).
Version namespaces shifted. v1: Jose\Signature\Algorithm\ES256. v2/v3: Jose\Component\Signature\Algorithm\ES256. Most stale Stack Overflow answers are v1. Check composer.json of whatever umbrella package you installed before copying code.
Do not roll your own hash_equals checks. JWSVerifier does constant-time comparison internally; re-implementing verification "just to add a claim check" reintroduces timing side channels. Verify first, then inspect $jws->getPayload().
Pin the minor version. The JWT Framework has had breaking changes between majors (v2 → v3 dropped several factory helpers), and this package's API is coupled to jwt-signature. A ^3 constraint from the umbrella keeps them in lockstep; installing the leaf package with an unconstrained version can drag in a mismatched jwt-core.
How can I help you explore Laravel packages today?