Signers & algorithms
Every signer and verifier is constructed for exactly one algorithm and pinned to it per call. There is no algorithm-agility switch, which makes algorithm-confusion attacks structurally impossible. All four families sign and verify:
| Algorithm | Key type | Curve / digest | Notes |
|---|---|---|---|
HS256 / HS384 / HS512 | HmacSecret | SHA-256/384/512 | Secret of at least 32 / 48 / 64 random bytes — the hash size (RFC 7518 §3.2). |
RS256 / RS384 / RS512 | RsaKey | RSA ≥ 2048, SHA-256/384/512 | The tier is a constructor argument. |
ES256 / ES384 / ES512 | EcKey | P-256 / P-384 / P-521 | The key’s curve fixes the digest. |
EdDSA | OkpKey | Ed25519 | Needs ext-sodium. |
Choosing an algorithm: prefer EdDSA or ES256 for new asymmetric tokens (small, fast); use RS256 for interop with systems that require RSA; use HS256 only when both sides share a secret. Pin the exact algorithm on verify() — the package never infers it from the token.
Through the facade
Each signer takes a key built with Crypto::keys(); Crypto::verifier() returns the key-driven verifier:
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Crypto\Signature\Algorithm;
$hs = Crypto::hs(Crypto::keys()->hmac()->fromConfig('tokens.secret'), Algorithm::HS512); // default HS256; HS512 needs ≥ 64 bytes
$rs = Crypto::rs(Crypto::keys()->rsa()->private($privatePem)); // default RS256
$es = Crypto::es(Crypto::keys()->ec()->generate('P-384')); // ES384, from the curve
$ed = Crypto::eddsa(Crypto::keys()->ed25519()->generate()); // needs ext-sodium
$sig = $es->sign($message); // raw r‖s, as JOSE wants it
$ok = Crypto::verifier()->verify($publicKey, $message, $sig); // the key picks the algorithmThe Signer and Verifier contracts
Hs, Rs, Es and EdDSA each implement both contracts. sign() returns raw signature bytes in the algorithm’s wire form — for ECDSA that is the JOSE raw r‖s, not DER. A verifier never inspects a token header to choose an algorithm:
interface Signer { public function algorithm(): Algorithm; public function sign(string $message): string; }
interface Verifier { public function algorithm(): Algorithm; public function verify(string $message, string $signature): bool; }Hs — HMAC
use RoundlyConsulting\Crypto\Signature\Hs;
use RoundlyConsulting\Crypto\Signature\Algorithm;
use RoundlyConsulting\Crypto\Signature\Key\HmacSecret;
$hs = new Hs(HmacSecret::fromString($secret), Algorithm::HS512); // default HS256; HS512 needs ≥ 64 bytes
$sig = $hs->sign($message);
$ok = $hs->verify($message, $sig); // constant-timeThe constructor rejects any non-HMAC algorithm (AlgorithmMismatchException), so an HMAC secret can never be pressed into service for RS, ES or EdDSA. It also refuses a secret shorter than its tier’s hash output — 48 bytes for HS384, 64 for HS512 (HmacSecret::generate(64)) — with a WeakKeyException, because verifiers that enforce RFC 7518 §3.2 reject every token such a key signs. Verification is constant-time.
Rs — RSA
use RoundlyConsulting\Crypto\Signature\Rs;
use RoundlyConsulting\Crypto\Signature\Key\RsaKey;
$signer = new Rs(RsaKey::private($privatePem), Algorithm::RS256); // default RS256
$sig = $signer->sign($message); // needs a private key
$verifier = new Rs(RsaKey::public($publicPem), Algorithm::RS256);
$ok = $verifier->verify($message, $sig);The digest tier is fixed at construction (default RS256); any RSA key of 2048–8192 bits works with any RS tier. Signing with a public-only key throws KeyLoadException::signingFailed(). An Rs built from a private key also verifies: the public half is derived for the check, because ext-openssl will not verify with a private-key handle.
Es — ECDSA
use RoundlyConsulting\Crypto\Signature\Es;
use RoundlyConsulting\Crypto\Signature\Key\EcKey;
$signer = new Es(EcKey::private($p256Pem)); // algorithm is read from the key's curve
$sig = $signer->sign($message); // JOSE raw r‖s form
$verifier = new Es(EcKey::public($p256PublicPem));
$ok = $verifier->verify($message, $sig);The curve — and therefore the coordinate size and digest — comes from the key itself, never from a header. The internal DER handling is strict and minimal. Raw ECDSA is malleable, so never use a signature as an idempotency, dedup or cache key. As with Rs, an Es built from a private key verifies its own signatures through the derived public half, on every curve.
EdDSA — Ed25519
use RoundlyConsulting\Crypto\Signature\EdDSA;
use RoundlyConsulting\Crypto\Signature\Key\OkpKey;
$key = OkpKey::generate(); // or OkpKey::fromSecretKey($sk)
$sig = (new EdDSA($key))->sign($message);
$ok = (new EdDSA(OkpKey::ed25519($key->publicKey)))->verify($message, $sig);Verification needs only the public key; signing needs the secret half. Without ext-sodium both throw Cose\UnsupportedAlgorithmException::sodiumMissing() rather than silently degrading. A signature that is not exactly 64 bytes fails verification.
Key-driven verification
KeyVerifier pins verification to a public key’s own algorithm — the entry point WebAuthn and JOSE key-driven flows use. ECDSA signatures are accepted either as DER (the form WebAuthn delivers) or as raw r‖s; other algorithms use their native wire form:
use RoundlyConsulting\Crypto\Signature\KeyVerifier;
// The key chooses the algorithm — never a caller-supplied header:
$ok = (new KeyVerifier)->verify($publicKey, $signedData, $signature);Algorithm helpers
use RoundlyConsulting\Crypto\Signature\Algorithm;
Algorithm::ES256->isAsymmetric(); // true
Algorithm::HS512->isHmac(); // true
Algorithm::RS384->hashName(); // 'sha384'
Algorithm::EdDSA->hashName(); // 'sha512'
Algorithm::ES256->opensslAlgorithm(); // OPENSSL_ALGO_SHA256ECDSA DER codec
Signature\Ec\Der converts between the raw r‖s that JOSE and WebAuthn deliver and the ASN.1 DER that ext-openssl produces. Es and KeyVerifier use it internally, but it is public — on the facade as Crypto::ecDer(). $coordBytes is 32 for P-256, 48 for P-384 and 66 for P-521. Invalid input throws InvalidSignatureException:
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Crypto\Signature\Ec\Der;
$der = Crypto::ecDer()->fromRaw($rawRS, 32); // raw r‖s → DER (32 = P-256 coordinate bytes)
$raw = Crypto::ecDer()->toRaw($der, 32); // DER → raw r‖s
Crypto::ecDer()->isValid($der); // a well-formed, minimally-encoded ECDSA DER signature?
// The same static helpers, no container:
Der::fromRaw($rawRS, 48); // 48 = P-384, 66 = P-521Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.