Pular para o conteúdo
ecdsa.com
PHP

PHP openssl extension

A extensão openssl embutida no PHP: assinaturas ECDSA em DER sobre as curvas do OpenSSL vinculado, tipos de chave Ed25519 e Ed448 e a função openssl_verify(), que retorna -1 — um valor avaliado como verdadeiro — para entradas malformadas.
Licença
PHP-3.01
Versão verificada
8.5.11 ·
Código-fonte
github.com/php/php-src
Documentação
www.php.net/manual/en/book.openssl.php

Visão geral da compatibilidade

ECDSAEdDSA

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

As funções openssl_sign(), openssl_verify() e openssl_pkey_* são camadas finas sobre o OpenSSL com que o PHP foi compilado — o 3.5.7 na imagem oficial php:8.5.11 —, por isso as curvas e o comportamento seguem os dele: assinaturas DER, nonces aleatórios, nenhuma normalização low-S e uma verificação que aceita high-S. O hash sempre vem do argumento de algoritmo; não existe função que assine um digest pré-calculado.

O valor de retorno de openssl_verify() é a armadilha clássica do PHP. A função retorna 1 para uma assinatura válida, 0 para uma inválida e -1 quando nem consegue interpretar a assinatura — foi o que uma assinatura r‖s bruta de 64 bytes vinda do WebCrypto ou de um JWT produziu no teste. No PHP, -1 é avaliado como verdadeiro, então if (openssl_verify(...)) aceita lixo; compare com === 1, como faz o exemplo. Assinaturas brutas precisam primeiro ser encapsuladas em DER, que é o que toda biblioteca JWT para PHP faz internamente; o conversor DER ⇄ bruto mostra as duas formas.

As chaves são passadas como strings PEM, certificados ou recursos de chave: SPKI PEM e X.509 PEM funcionaram, enquanto bytes SPKI DER brutos foram rejeitados de imediato. As chaves privadas são carregadas de PKCS#8 (simples ou criptografado) e de SEC1 EC PRIVATE KEY, e openssl_pkey_export() grava PKCS#8. As curvas cobertas vão da P-224 à P-521, além da secp256k1 e das Brainpool, e as chaves Ed25519 e Ed448 funcionam por meio de OPENSSL_KEYTYPE_ED25519 e OPENSSL_KEYTYPE_ED448 com o algoritmo 0.

Os algoritmos pós-quânticos não são acessíveis a partir do código PHP, embora o OpenSSL vinculado os implemente: a CLI da mesma imagem listou o ML-DSA, mas a extensão não define nenhum tipo de chave para ele. Para JWTs, use o firebase/php-jwt, o web-token ou o lcobucci/jwt em vez de montar tokens à mão.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/php-openssl/run.sh executou PHP openssl extension 8.5.11 em OpenSSL 3.5.7 9 Jun 2026 na imagem de contêiner php:8.5.11-cli-trixie em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    der

    openssl_sign() retorna ASN.1 DER.

    Registrado pela verificação
    fact ecdsa-signature-encoding = der
  • Nonce do ECDSA

    random

    Nonce aleatório: duas assinaturas da mesma mensagem são diferentes.

    Registrado pela verificação
    fact ecdsa-nonce = random
  • Low-S ao assinar

    either

    O s não é normalizado: valores high-S apareceram entre 64 assinaturas.

    Registrado pela verificação
    fact ecdsa-low-s-sign = either
  • High-S ao verificar

    accepts-high-s

    openssl_verify() retornou 1 para a assinatura com s substituído por n − s.

    Registrado pela verificação
    fact ecdsa-low-s-verify = accepts-high-s
  • Entrada do hash na chamada de assinatura

    hashes-message

    O hash dos dados é sempre calculado com o argumento de algoritmo; não existe função para assinar um digest.

    Registrado pela verificação
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: openssl_sign() always hashes with the algorithm given; there is no digest-signing function
  • Assinatura ECDSA malformada

    returns-error

    Uma assinatura r‖s bruta e um blob de 3 bytes fazem openssl_verify() retornar -1; uma assinatura válida sobre outra mensagem retorna 0.

    Registrado pela verificação
    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 da verificação

    -10

    Além de 1 para assinatura válida, o teste obteve -1 (não interpretável) e 0 (assinatura incorreta); no PHP, (bool) -1 é true.

    Registrado pela verificação
    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

    Assinatura e verificação funcionaram com P-224, P-256, P-384, P-521, secp256k1 e brainpoolP256r1/P384r1/P512r1.

    Registrado pela verificação
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 e Ed448 por meio de OPENSSL_KEYTYPE_ED25519 e OPENSSL_KEYTYPE_ED448; as funções sodium_crypto_sign_* da libsodium também vêm embutidas.

    Registrado pela verificação
    fact eddsa = Ed25519, Ed448
    evidence eddsa: OPENSSL_KEYTYPE_ED25519 defined: true; sodium_crypto_sign available: true
  • Algoritmos pós-quânticos

    none

    A extensão não tem constantes de tipo de chave pós-quântico, embora a CLI do OpenSSL da imagem liste o ML-DSA.

    Registrado pela verificação
    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 chave pública

    spki-pemx509

    SPKI PEM e certificados X.509 em PEM; bytes SPKI DER brutos foram rejeitados.

    Registrado pela verificação
    fact public-key-formats = spki-pem, x509
    evidence public-key-formats: SPKI DER bytes passed as the key: openssl_verify returned false
  • Formatos de chave privada

    pkcs8-pemencrypted-pkcs8sec1-pem

    PKCS#8 PEM, PKCS#8 criptografado e SEC1 EC PRIVATE KEY; openssl_pkey_export() grava PKCS#8.

    Registrado pela verificação
    fact private-key-formats = pkcs8-pem, encrypted-pkcs8, sec1-pem
    evidence private-key-formats: openssl_pkey_export writes -----BEGIN PRIVATE KEY-----

Uso

Assine com P-256 e veja por que os resultados de openssl_verify() precisam ser comparados com === 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";

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner php:8.5.11-cli-trixie, e a execução falha se a saída não corresponder.

Comando
php usage.php 2>/dev/null
A saída deve corresponder a (expressão regular)
valid: true, raw 64 bytes gives -1, which is truthy: true

Bibliotecas