Saltar al contenido
ecdsa.com
PHP

PHP openssl extension

La extensión openssl incluida en PHP: firmas ECDSA DER sobre las curvas del OpenSSL enlazado, tipos de clave Ed25519 y Ed448, y un openssl_verify() que devuelve -1 —un valor que PHP evalúa como verdadero— con una entrada mal formada.
Licencia
PHP-3.01
Versión comprobada
8.5.11 ·
Código fuente
github.com/php/php-src
Documentación
www.php.net/manual/en/book.openssl.php

Compatibilidad de un vistazo

ECDSAEdDSA

Curvas ECDSA
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Formatos de clave privada
pkcs8-pemencrypted-pkcs8sec1-pem

openssl_sign(), openssl_verify() y las funciones openssl_pkey_* son envoltorios finos sobre el OpenSSL con el que se compiló PHP —3.5.7 en la imagen oficial php:8.5.11—, así que las curvas y el comportamiento son los suyos: firmas DER, nonces aleatorios, sin normalización low-S y una verificación que acepta high-S. El hash siempre viene del argumento de algoritmo; no hay ninguna función que firme un resumen precalculado.

El valor de retorno de openssl_verify() es la trampa clásica de PHP. Devuelve 1 para una firma válida, 0 para una no válida y -1 cuando la firma ni siquiera se puede analizar, que es lo que produjo en la comprobación una firma r‖s en bruto de 64 bytes procedente de WebCrypto o de un JWT. En PHP, -1 se evalúa como verdadero, así que if (openssl_verify(...)) acepta basura; compara con === 1, como hace el ejemplo. Las firmas en bruto deben envolverse antes en DER, que es lo que hace internamente toda biblioteca JWT de PHP; el conversor DER ⇄ en bruto muestra las dos formas.

Las claves se pasan como cadenas PEM, certificados o recursos de clave: SPKI PEM y X.509 PEM funcionaron, mientras que los bytes SPKI DER en bruto se rechazaron de plano. Las claves privadas se cargan desde PKCS#8 (sin cifrar o cifrado) y desde SEC1 EC PRIVATE KEY, y openssl_pkey_export() escribe PKCS#8. Las curvas abarcaron de P-224 a P-521, secp256k1 y Brainpool, y las claves Ed25519 y Ed448 funcionan mediante OPENSSL_KEYTYPE_ED25519 y OPENSSL_KEYTYPE_ED448 con el algoritmo 0.

Los algoritmos poscuánticos no son accesibles desde código PHP, aunque el OpenSSL enlazado los implementa: la CLI de la misma imagen enumeró ML-DSA, pero la extensión no define ningún tipo de clave para él. Para JWT, usa firebase/php-jwt, web-token o lcobucci/jwt en lugar de montar los tokens a mano.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/php-openssl/run.sh ejecutó PHP openssl extension 8.5.11 en OpenSSL 3.5.7 9 Jun 2026 en la imagen de contenedor php:8.5.11-cli-trixie el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    der

    openssl_sign() devuelve DER ASN.1.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = der
  • Nonce de ECDSA

    random

    Nonce aleatorio: dos firmas del mismo mensaje son distintas.

    Registrado por la comprobación
    fact ecdsa-nonce = random
  • Low-S al firmar

    either

    s no se normaliza: aparecieron valores high-S entre 64 firmas.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = either
  • High-S al verificar

    accepts-high-s

    openssl_verify() devolvió 1 para la firma con s sustituido por n − s.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
  • De qué calcula el hash la llamada de firma

    hashes-message

    Siempre se calcula el hash de los datos con el argumento de algoritmo; no existe ninguna función que firme un resumen.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: openssl_sign() always hashes with the algorithm given; there is no digest-signing function
  • Firma ECDSA mal formada

    returns-error

    Una firma r‖s en bruto y un blob de 3 bytes hacen que openssl_verify() devuelva -1; una firma válida sobre otro mensaje devuelve 0.

    Registrado por la comprobación
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: raw r||s: openssl_verify returned -1
    evidence ecdsa-malformed-signature: 3 bytes: openssl_verify returned -1
    evidence ecdsa-malformed-signature: wrong message: openssl_verify returned 0
  • Valores de retorno de la verificación

    -10

    Además de 1 para una firma válida, la comprobación obtuvo -1 (no analizable) y 0 (firma incorrecta); (bool) -1 es true en PHP.

    Registrado por la comprobación
    fact verify-return-values = -1, 0
    evidence verify-return-values: -1 is truthy in PHP: (bool) -1 === true
  • Curvas ECDSA

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    Con P-224, P-256, P-384, P-521, secp256k1 y brainpoolP256r1/P384r1/P512r1 se firmó y se verificó.

    Registrado por la comprobación
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 y Ed448 mediante OPENSSL_KEYTYPE_ED25519 y OPENSSL_KEYTYPE_ED448; también se incluye sodium_crypto_sign_* de libsodium.

    Registrado por la comprobación
    fact eddsa = Ed25519, Ed448
    evidence eddsa: OPENSSL_KEYTYPE_ED25519 defined: true; sodium_crypto_sign available: true
  • Algoritmos poscuánticos

    none

    La extensión no tiene constantes de tipo de clave poscuánticas, aunque la CLI de OpenSSL de la imagen enumera ML-DSA.

    Registrado por la comprobación
    fact pqc = none
    evidence pqc: the openssl CLI of the same image lists 3 ML-DSA signature algorithm lines; the extension has no ML-DSA key type constant (openssl_pkey_new with an unknown type: false)
  • Formatos de clave pública

    spki-pemx509

    SPKI PEM y certificados X.509 en PEM; los bytes SPKI DER en bruto se rechazaron.

    Registrado por la comprobación
    fact public-key-formats = spki-pem, x509
    evidence public-key-formats: SPKI DER bytes passed as the key: openssl_verify returned false
  • Formatos de clave privada

    pkcs8-pemencrypted-pkcs8sec1-pem

    PKCS#8 PEM, PKCS#8 cifrado y SEC1 EC PRIVATE KEY; openssl_pkey_export() escribe PKCS#8.

    Registrado por la comprobación
    fact private-key-formats = pkcs8-pem, encrypted-pkcs8, sec1-pem
    evidence private-key-formats: openssl_pkey_export writes -----BEGIN PRIVATE KEY-----

Uso

Firma con P-256 y muestra por qué los resultados de openssl_verify() deben compararse con === 1.
<?php
$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
$publicPem = openssl_pkey_get_details($key)['key'];
$message = 'order #1042 approved';

openssl_sign($message, $der, $key, OPENSSL_ALGO_SHA256); // ASN.1 DER

// openssl_verify returns 1 (valid), 0 (invalid) or -1 (error). -1 is truthy,
// so compare strictly with === 1, never with a loose if ($result).
$valid = openssl_verify($message, $der, $publicPem, OPENSSL_ALGO_SHA256);
$garbage = openssl_verify($message, random_bytes(64), $publicPem, OPENSSL_ALGO_SHA256);

echo 'valid: ', var_export($valid === 1, true), ', raw 64 bytes gives ', $garbage, ', which is truthy: ', var_export((bool) $garbage, true), "\n";

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor php:8.5.11-cli-trixie, y la ejecución falla si la salida no coincide.

Comando
php usage.php 2>/dev/null
La salida debe coincidir (expresión regular)
valid: true, raw 64 bytes gives -1, which is truthy: true

Bibliotecas