studio-design / ecdsa-signature
ECDSA signature value object with DER/JWS raw format conversion and curve-order validation. For Cloud KMS/HSM-based JWT signing where OpenSSL/KMS returns DER but JWT (RFC 7518) requires raw format.
Requires
- php: ^8.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
This package is auto-updated.
Last update: 2026-04-09 03:43:37 UTC
README
A lightweight PHP value object for ECDSA signatures with DER/JWS raw format conversion and mathematical validation.
Why?
OpenSSL and Cloud KMS (Google Cloud KMS, AWS KMS, Azure Key Vault) return ECDSA signatures in ASN.1 DER format. However, JWT/JWS (RFC 7518 Section 3.4) requires raw concatenated R || S format with fixed-length components.
Major PHP JWT libraries (firebase/php-jwt, lcobucci/jwt, web-token/jwt-library) all handle this conversion internally but expose it only as private or @internal methods. If you're signing JWTs via Cloud KMS or an HSM — where the private key never leaves the remote service — you need this conversion as a standalone utility.
This library provides an immutable value object that guarantees both format correctness and mathematical validity (0 < r, s < n).
Non-Goals
This library does not:
- Verify ECDSA signatures (use
openssl_verify()or your KMS provider for that) - Generate or manage keys
- Build or parse JWTs
- Support non-NIST curves (e.g. secp256k1, Ed25519)
It is a signature format converter + validated value object, not a cryptography library.
Installation
composer require studio-design/ecdsa-signature
Requirements
- PHP 8.2+
No extensions required. No external dependencies.
Quick Start
DER to Raw (for JWT signing)
Convert a DER-encoded ECDSA signature (from OpenSSL or Cloud KMS) to JWS raw format:
use StudioDesign\EcdsaSignature\Curve; use StudioDesign\EcdsaSignature\EcdsaSignature; // Sign with OpenSSL (returns DER format) openssl_sign($payload, $derSignature, $privateKey, OPENSSL_ALGO_SHA256); // Parse DER and convert to JWS raw format (R||S) $sig = EcdsaSignature::fromDer($derSignature, Curve::P256); $rawSignature = $sig->toRaw(); // $rawSignature is now 64 bytes (32-byte R + 32-byte S)
Raw to DER (for signature verification)
Convert a JWS raw signature back to DER format for OpenSSL verification:
$sig = EcdsaSignature::fromRaw($rawSignature, Curve::P256); $derSignature = $sig->toDer(); // Verify with OpenSSL (expects DER format) $result = openssl_verify($payload, $derSignature, $publicKey, OPENSSL_ALGO_SHA256);
Choosing a Curve
Multiple ways to select a curve, depending on your context:
use StudioDesign\EcdsaSignature\Curve; // Direct enum usage $curve = Curve::P256; // From a JOSE algorithm name (JWT header "alg" field) $curve = Curve::fromJoseAlg('ES256'); // → Curve::P256 $curve = Curve::fromJoseAlg('ES384'); // → Curve::P384 $curve = Curve::fromJoseAlg('ES512'); // → Curve::P521 // From an OpenSSL curve name $curve = Curve::fromOpenSslCurveName('prime256v1'); // → Curve::P256 $curve = Curve::fromOpenSslCurveName('secp384r1'); // → Curve::P384 $curve = Curve::fromOpenSslCurveName('secp521r1'); // → Curve::P521 // From the JOSE key-size integer (256, 384, 512) // Note: Curve::from() is PHP's built-in enum method and throws ValueError, // not EcdsaSignatureException. Prefer fromJoseAlg() or fromOpenSslCurveName() // for consistent error handling. $curve = Curve::from(256); // → Curve::P256 // Reverse lookups $curve->joseAlg(); // "ES256" $curve->openSslCurveName(); // "prime256v1"
Usage Examples
JWT with Cloud KMS
use StudioDesign\EcdsaSignature\Curve; use StudioDesign\EcdsaSignature\EcdsaSignature; // 1. Determine curve from JWT algorithm $alg = 'ES256'; $curve = Curve::fromJoseAlg($alg); // 2. Build JWT header and payload $header = base64url_encode(json_encode(['alg' => $alg, 'typ' => 'JWT', 'kid' => $kid])); $payload = base64url_encode(json_encode($claims)); $signingInput = "{$header}.{$payload}"; // 3. Send digest to Cloud KMS for signing (returns DER) $digest = hash('sha256', $signingInput, binary: true); $derSignature = $kmsClient->asymmetricSign($keyName, $digest); // 4. Convert DER signature to JWS raw format $sig = EcdsaSignature::fromDer($derSignature, $curve); // 5. Assemble JWT $jwt = "{$signingInput}." . base64url_encode($sig->toRaw());
OpenSSL Sign and Verify
use StudioDesign\EcdsaSignature\Curve; use StudioDesign\EcdsaSignature\EcdsaSignature; $curve = Curve::fromOpenSslCurveName('prime256v1'); // Sign (OpenSSL produces DER) $key = openssl_pkey_new([ 'ec' => ['curve_name' => $curve->openSslCurveName()], 'private_key_type' => OPENSSL_KEYTYPE_EC, ]); openssl_sign($payload, $der, $key, OPENSSL_ALGO_SHA256); // Convert to raw for JWT $raw = EcdsaSignature::fromDer($der, $curve)->toRaw(); // Later: convert raw back to DER for verification $derAgain = EcdsaSignature::fromRaw($raw, $curve)->toDer(); openssl_verify($payload, $derAgain, $publicKey, OPENSSL_ALGO_SHA256);
Accessing Components
$sig = EcdsaSignature::fromRaw($rawSignature, Curve::P256); $sig->r(); // R component (32 bytes, fixed-length big-endian binary) $sig->s(); // S component (32 bytes, fixed-length big-endian binary) $sig->curve(); // Curve::P256
Error Handling
The library provides a structured exception hierarchy. All exceptions extend both EcdsaSignatureException and PHP's built-in InvalidArgumentException, so existing catch (InvalidArgumentException) blocks continue to work.
use StudioDesign\EcdsaSignature\Exception\EcdsaSignatureException; use StudioDesign\EcdsaSignature\Exception\InvalidDerSignature; use StudioDesign\EcdsaSignature\Exception\InvalidRawSignature; use StudioDesign\EcdsaSignature\Exception\InvalidSignatureComponent; use StudioDesign\EcdsaSignature\Exception\UnsupportedCurve; try { $curve = Curve::fromJoseAlg($alg); $sig = EcdsaSignature::fromDer($input, $curve); } catch (UnsupportedCurve $e) { // Algorithm name or curve name is not supported } catch (InvalidDerSignature $e) { // DER structure is malformed (bad tag, truncated, non-minimal encoding, etc.) } catch (InvalidSignatureComponent $e) { // R or S is mathematically out of range (zero, >= curve order, oversized) } try { $sig = EcdsaSignature::fromRaw($input, $curve); } catch (InvalidRawSignature $e) { // Wrong byte length for the given curve } catch (InvalidSignatureComponent $e) { // R or S is mathematically out of range } // Or catch everything from this library at once: try { $curve = Curve::fromJoseAlg($alg); $sig = EcdsaSignature::fromDer($input, $curve); } catch (EcdsaSignatureException $e) { // Any error from this library — curve resolution, DER parsing, or range validation }
| Exception | Thrown by | Meaning |
|---|---|---|
UnsupportedCurve |
Curve::fromJoseAlg(), Curve::fromOpenSslCurveName() |
Curve identifier is not supported |
InvalidDerSignature |
EcdsaSignature::fromDer() |
DER structure is malformed |
InvalidRawSignature |
EcdsaSignature::fromRaw() |
Raw signature has wrong byte length |
InvalidSignatureComponent |
EcdsaSignature::fromDer(), EcdsaSignature::fromRaw() |
R or S fails 0 < value < n check |
EcdsaSignatureException |
(base class) | Any of the above |
Supported Curves
| JOSE Algorithm | Curve Enum | OpenSSL Name | Curve | Raw Signature Length |
|---|---|---|---|---|
| ES256 | Curve::P256 |
prime256v1 |
P-256 | 64 bytes |
| ES384 | Curve::P384 |
secp384r1 |
P-384 | 96 bytes |
| ES512 | Curve::P521 |
secp521r1 |
P-521 | 132 bytes |
Validation
Both fromDer() and fromRaw() validate that signature components satisfy 0 < r, s < n (where n is the curve order). Signatures with zero-valued or out-of-range components are rejected.
fromDer() additionally enforces strict DER encoding rules per X.690: minimal integer encoding, proper tag/length structure, no trailing data.
This ensures that every EcdsaSignature instance represents a mathematically plausible ECDSA signature.
API Reference
EcdsaSignature::fromDer(string $der, Curve $curve): self
Parse a DER-encoded ECDSA signature.
- Throws
InvalidDerSignatureif the DER data is structurally malformed - Throws
InvalidSignatureComponentif R or S is out of range
EcdsaSignature::fromRaw(string $raw, Curve $curve): self
Parse a JWS raw (R||S) ECDSA signature.
- Throws
InvalidRawSignatureif the byte length is wrong for the given curve - Throws
InvalidSignatureComponentif R or S is out of range
EcdsaSignature::toDer(): string
Encode this signature as ASN.1 DER.
EcdsaSignature::toRaw(): string
Encode this signature as JWS raw (R||S) format.
EcdsaSignature::r(): string / EcdsaSignature::s(): string
Fixed-length R/S components as big-endian binary strings.
EcdsaSignature::curve(): Curve
The elliptic curve this signature belongs to.
Curve enum
Curve::P256 // ES256, backing value 256 Curve::P384 // ES384, backing value 384 Curve::P521 // ES512, backing value 512 // Factory methods (recommended) Curve::fromJoseAlg('ES256'); // From JOSE algorithm name Curve::fromOpenSslCurveName('prime256v1'); // From OpenSSL curve name Curve::from(256); // PHP built-in enum factory (throws ValueError, not EcdsaSignatureException) // Properties $curve->componentLength(); // Per-component byte length (32, 48, 66) $curve->order(); // Curve order as fixed-length binary string $curve->joseAlg(); // JOSE algorithm name ("ES256", "ES384", "ES512") $curve->openSslCurveName(); // OpenSSL curve name ("prime256v1", "secp384r1", "secp521r1")
Background
ECDSA produces two integer values (r, s). These can be encoded in two ways:
- ASN.1 DER — Variable-length encoding:
SEQUENCE { INTEGER r, INTEGER s }. This is what OpenSSL and Cloud KMS return. - JWS Raw (IEEE P1363) — Fixed-length concatenation:
R || S, each padded to the key size. This is what JWT/JWS requires per RFC 7518.
The conversion handles:
- Stripping/adding DER sign-padding bytes (leading
0x00for positive integers with high bit set) - Padding/trimming R and S to fixed-length components
- Validating DER structure integrity (SEQUENCE tag, length fields, trailing data detection)
- Validating mathematical range (
0 < r, s < n)
License
MIT License. See LICENSE for details.