public-square/jwt-framework-schnorr
Adds Schnorr signature support to JWT workflows in PHP and Laravel, letting you issue and verify compact, tamper-evident tokens using modern public-key cryptography. A focused integration for developers needing Schnorr-based JWT signing.
This is a cryptography package (Schnorr signatures, with an experimental SS256K curve) at v0.1.0. Treat it as pre-stable: the API surface is still moving, so read README.md, src/, and tests/ before writing application code against it.
Install and pin. Because this is a 0.x release, SemVer allows breaking changes in minor bumps. Pin it:
composer require <vendor>/<package>:0.1.0
Check the Composer constraints in composer.json first — crypto packages commonly require ext-gmp or ext-bcmath and a minimum PHP version. Confirm these on your CI image and in config/platform.php before deploying.
Where to look first, in order:
README.md — quickstart for key generation, sign, verify.tests/ — the new testing suite is the best documentation of intended usage; the fixtures likely include known-answer test vectors.src/ — the public interfaces (signer, key pair, signature value objects). These, not the concrete classes, are what you should type-hint.First use case. A single end-to-end round trip, written as a test rather than a tinker session:
$keyPair = $signer->generateKeyPair(); // or fromSecretKey(config(...))
$signature = $signer->sign($message, $keyPair->privateKey());
$signer->verify($message, $signature, $keyPair->publicKey()); // true
Get that green before wiring anything into a controller. If it fails, the problem is almost always encoding (hex vs. raw binary) or a missing PHP extension — not your application code.
Wrap the package behind your own interface. This is the single most valuable thing you can do at v0.1.0. Bind the package to a narrow app-owned contract so a breaking change in 0.2.0 is a one-file edit:
interface PayloadSigner {
public function sign(string $payload): string; // returns hex
public function verify(string $payload, string $sig, string $pubKey): bool;
}
$this->app->singleton(PayloadSigner::class, fn () => new SchnorrPayloadSigner(
secretKey: config('signing.secret_key'),
));
Everything else in the app — middleware, jobs, form requests — depends on PayloadSigner, never on the package namespace.
Verification as middleware or a form request. For signed webhooks or API requests, verify the detached signature in one place and reject early:
public function handle($request, Closure $next)
{
$ok = $this->signer->verify(
$request->getContent(), // raw body, not parsed
$request->header('X-Signature'),
$request->header('X-Public-Key'),
);
abort_unless($ok, 401);
return $next($request);
}
The raw body matters — $request->all() re-serialises and the bytes will not match.
Keys come from config or a vault, never from code. Put the secret key in .env as hex, read it through a config key, and keep that config file out of anything you log or cache. For anything beyond an internal service, load from a KMS/secret manager at boot and cache the resolved key pair as a singleton so you parse it once per request lifecycle.
Test against vectors, not against yourself. Sign-then-verify proves nothing about correctness — a broken implementation round-trips fine. Use the published test vectors the new testing suite ships with, and add a cross-implementation check (sign in PHP, verify in libsodium/another language) in CI if the signatures cross a trust boundary.
Fake it in feature tests. Bind a deterministic fake PayloadSigner for HTTP tests so you are testing your middleware and routing, not the cryptography. Keep a separate, small integration suite that runs the real signer.
v0.1.0 is a warning, not a version number. Expect a renamed method or changed signature at any minor bump. Pin exactly, upgrade deliberately, and read the diff.bin2hex mismatch, or a body that picked up whitespace or a trailing newline in transit.php artisan config:cache. Reading .env directly inside the package or your wrapper works in local and silently breaks in a cached-config deploy. Always go through config().report() hook or a redaction processor before you ship.=== on secrets and signature comparisons should be hash_equals. If the package's verify does not document constant-time comparison, wrap or patch it.dd(). Assert on bin2hex(), lengths (32-byte key, 64-byte Schnorr signature), and vector equality.Signature/KeyPair value object you can extend, and a service-provider binding you can override in AppServiceProvider. Favour configuring these over forking.composer update --with-dependencies in a branch, and check that no transitive dependency pulled in a conflicting ext- requirement before merging.How can I help you explore Laravel packages today?