---
title: "Signers & algorithms — Crypto for Laravel | Roundly"
description: "HS, RS, ES and EdDSA signers pinned to exactly one algorithm each, key-driven verification and the ECDSA DER codec."
url: https://roundly-consulting.com/open-source/docs/crypto-for-laravel/signatures
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Crypto for Laravel](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)

# 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:

```php
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 algorithm
```

## The 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:

```php
interface Signer   { public function algorithm(): Algorithm; public function sign(string $message): string; }
interface Verifier { public function algorithm(): Algorithm; public function verify(string $message, #[\SensitiveParameter] string $signature): bool; }
```

Every package verifier marks the signature #\[\\SensitiveParameter\]: beside the signing input it rebuilds a JWS bearer token, so it never belongs in a stack trace. PHP does not carry the attribute over from the interface, so a Verifier of your own must mark its $signature too.

## Hs — HMAC

```php
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-time
```

The 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

```php
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

```php
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

```php
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. Hand the public half to a verifier outside the package as a PEM with publicPem() — see Loading & generating keys.

## 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:

```php
use RoundlyConsulting\Crypto\Signature\KeyVerifier;

// The key chooses the algorithm — never a caller-supplied header:
$ok = (new KeyVerifier)->verify($publicKey, $signedData, $signature);
```

## Algorithm helpers

```php
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_SHA256
```

## ECDSA 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:

```php
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-521
```

## Show 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)
