starkbank/ecdsa
Pure-PHP ECDSA implementation compatible with OpenSSL. Fast signing/verification using Jacobian coordinates and optimized scalar multiplication. Security features include RFC6979 deterministic nonces, low-S normalization, on-curve validation, and hash truncation. Supports secp256k1 and P-256; requir...
Installation:
composer require starkbank/ecdsa
Ensure ext-gmp is enabled in your PHP environment (php -m | grep gmp).
First Use Case: Sign a message with a generated key pair:
use EllipticCurve\PrivateKey;
use EllipticCurve\Ecdsa;
$privateKey = new PrivateKey(); // Generates secp256k1 by default
$message = "Hello, Laravel!";
$signature = Ecdsa::sign($message, $privateKey);
$publicKey = $privateKey->publicKey();
// Verify
$isValid = Ecdsa::verify($message, $signature, $publicKey);
Key Sources:
new PrivateKey().$privateKey = PrivateKey::fromPem(file_get_contents('key.pem'));
EllipticCurve\CurveFp for supported curves (secp256k1, prime256v1) and how to add custom ones.// Sign a JSON payload (e.g., for API requests)
$message = json_encode(['user_id' => 123, 'amount' => 100]);
$signature = Ecdsa::sign($message, $privateKey);
$base64Sig = $signature->toBase64(); // For API headers/body
// Validate a signature (e.g., in a Laravel middleware)
$publicKey = PublicKey::fromPem($request->header('public-key'));
$isValid = Ecdsa::verify($request->getContent(), $signature, $publicKey);
// Rotate keys periodically (e.g., in a Laravel job)
$oldPrivateKey = PrivateKey::fromPem($oldKeyPem);
$newPrivateKey = new PrivateKey();
$oldPublicKey = $oldPrivateKey->publicKey();
$newPublicKey = $newPrivateKey->publicKey();
// Re-sign data with new key
$resignedSig = Ecdsa::sign($message, $newPrivateKey);
// Convert OpenSSL-generated keys to this library
$opensslPrivateKey = file_get_contents('private.pem');
$privateKey = PrivateKey::fromPem($opensslPrivateKey);
// Use existing OpenSSL signatures (DER format)
$derSig = file_get_contents('signature.der');
$signature = Signature::fromDer($derSig);
// config/app.php
'providers' => [
App\Providers\EcdsaServiceProvider::class,
],
// app/Providers/EcdsaServiceProvider.php
namespace App\Providers;
use EllipticCurve\PrivateKey;
use Illuminate\Support\ServiceProvider;
class EcdsaServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('ecdsa.private-key', function () {
return PrivateKey::fromPem(config('ecdsa.private_key'));
});
}
}
// app/Http/Middleware/ValidateSignature.php
namespace App\Http\Middleware;
use EllipticCurve\Ecdsa;
use EllipticCurve\PublicKey;
use Closure;
class ValidateSignature
{
public function handle($request, Closure $next)
{
$publicKey = PublicKey::fromPem($request->header('public-key'));
$signature = Signature::fromBase64($request->header('signature'));
if (!Ecdsa::verify($request->getContent(), $signature, $publicKey)) {
abort(403, 'Invalid signature');
}
return $next($request);
}
}
// app/Console/Commands/GenerateEcdsaKeys.php
namespace App\Console\Commands;
use EllipticCurve\PrivateKey;
use Illuminate\Console\Command;
class GenerateEcdsaKeys extends Command
{
protected $signature = 'ecdsa:generate';
protected $description = 'Generate ECDSA key pair';
public function handle()
{
$privateKey = new PrivateKey();
$publicKey = $privateKey->publicKey();
$this->info("Private Key:\n" . $privateKey->toPem());
$this->info("Public Key:\n" . $publicKey->toPem());
}
}
// config/ecdsa.php
return [
'private_key' => env('ECDSA_PRIVATE_KEY'),
'public_key' => env('ECDSA_PUBLIC_KEY'),
'curve' => 'secp256k1', // or 'prime256v1'
];
Hashing Messages:
Use hash() or hash_hmac() before signing to ensure deterministic outputs:
$hashedMessage = hash('sha256', $message, true);
$signature = Ecdsa::sign($hashedMessage, $privateKey);
Compressed Public Keys: Reduce storage/bandwidth for public keys:
$compressedKey = $publicKey->toCompressed(); // e.g., "02..."
$publicKey = PublicKey::fromCompressed($compressedKey);
Custom Curves:
Add support for ed25519 or other curves:
$ed25519Curve = new CurveFp(
'0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed',
'0x5866666666666666666666666666666666666666666666666666666666666666',
// ... other params
'ed25519'
);
CurveFp::add($ed25519Curve);
Batch Verification: For high-throughput APIs, preload public keys and verify in bulk:
$publicKeys = collect($request->public_keys)->map(fn($key) => PublicKey::fromPem($key));
$signatures = collect($request->signatures)->map(fn($sig) => Signature::fromBase64($sig));
$messages = $request->messages;
$results = $publicKeys->zip($signatures)->zip($messages)
->map(fn($tuple) => Ecdsa::verify(...$tuple));
Caching Signatures: Cache verified signatures for repeated requests (e.g., webhooks):
$cacheKey = "ecdsa:verify:{$request->id}";
if (!cache()->has($cacheKey)) {
$isValid = Ecdsa::verify($request->content, $signature, $publicKey);
cache()->put($cacheKey, $isValid, now()->addHours(1));
}
GMP Extension Missing:
FatalError: Uncaught Error: Call to undefined function gmp_init().ext-gmp in php.ini or use a Docker image with GMP (e.g., php:8.2-gmp).ext-openssl or a polyfill like php-gmp.Nonce Reuse:
Ecdsa::sign() (which implements RFC 6979) instead of manual key generation.Curve Mismatch:
Invalid curve parameter when verifying signatures.secp256k1 vs. prime256v1).Signature Malformation:
Signature malformed during verification.How can I help you explore Laravel packages today?