跳到主要内容
ecdsa.com
PHP

PHP openssl extension

PHP 自带的 openssl 扩展:基于所链接 OpenSSL 支持的曲线生成 DER ECDSA 签名,支持 Ed25519 和 Ed448 密钥类型;对于格式错误的输入,openssl_verify() 返回 -1——一个会被视为真的值。
许可证
PHP-3.01
检查的版本
8.5.11 ·
源代码
github.com/php/php-src
文档
www.php.net/manual/en/book.openssl.php

兼容性概览

ECDSAEdDSA

ECDSA 曲线
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
公钥格式
spki-pemx509
私钥格式
pkcs8-pemencrypted-pkcs8sec1-pem

openssl_sign()、openssl_verify() 和 openssl_pkey_* 系列函数是对 PHP 构建时所用 OpenSSL 的薄封装——官方 php:8.5.11 镜像中为 3.5.7——因此曲线和行为都与之一致:DER 签名、随机 nonce、不做 low-S 规范化,验证时接受 high-S。哈希算法始终由算法参数决定;没有可以对预先计算的摘要签名的函数。

openssl_verify() 的返回值是 PHP 中的经典陷阱。签名有效时返回 1,无效时返回 0,签名根本无法解析时返回 -1——在检查中,来自 WebCrypto 或 JWT 的 64 字节原始 r‖s 签名得到的就是这个结果。在 PHP 中 -1 为真,因此 if (openssl_verify(...)) 会接受垃圾数据;应该像示例那样用 === 1 比较。原始签名必须先封装为 DER,每个 PHP JWT 库在内部都是这样做的;DER ⇄ 原始格式转换器可以展示这两种格式。

密钥以 PEM 字符串、证书或密钥资源的形式传入:SPKI PEM 和 X.509 PEM 可以使用,而裸 SPKI DER 字节会被直接拒绝。私钥可以从 PKCS#8(未加密或加密)和 SEC1 EC PRIVATE KEY 加载,openssl_pkey_export() 写出 PKCS#8。曲线涵盖 P-224 到 P-521、secp256k1 和 Brainpool;Ed25519 和 Ed448 密钥通过 OPENSSL_KEYTYPE_ED25519 和 OPENSSL_KEYTYPE_ED448 使用,算法参数为 0。

尽管所链接的 OpenSSL 实现了后量子算法,PHP 代码却无法使用它们:同一镜像中的 CLI 列出了 ML-DSA,但扩展没有为它定义密钥类型。处理 JWT 时,应使用 firebase/php-jwt、web-token 或 lcobucci/jwt,而不是手动拼装令牌。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/php-openssl/run.sh 于2026年10月20日在容器镜像 php:8.5.11-cli-trixie 中运行了 PHP openssl extension 8.5.11(运行时为 OpenSSL 3.5.7 9 Jun 2026);本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der

    openssl_sign() 返回 ASN.1 DER。

    检查记录
    fact ecdsa-signature-encoding = der
  • ECDSA 的 nonce

    random

    随机 nonce:同一条消息的两个签名不同。

    检查记录
    fact ecdsa-nonce = random
  • 签名时的 low-S

    either

    s 没有经过规范化:64 个签名中出现了 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = either
  • 验证时的 high-S

    accepts-high-s

    对于把 s 替换为 n − s 的签名,openssl_verify() 返回 1。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • 签名调用对哪些内容做哈希

    hashes-message

    数据始终用算法参数指定的算法计算哈希;不存在对摘要签名的函数。

    检查记录
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: openssl_sign() always hashes with the algorithm given; there is no digest-signing function
  • 格式错误的 ECDSA 签名

    returns-error

    原始 r‖s 签名和 3 字节的数据块会让 openssl_verify() 返回 -1;针对另一条消息的有效签名返回 0。

    检查记录
    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
  • 验证的返回值

    -10

    除了表示有效的 1,检查中还出现了 -1(无法解析)和 0(签名错误);在 PHP 中 (bool) -1 为 true。

    检查记录
    fact verify-return-values = -1, 0
    evidence verify-return-values: -1 is truthy in PHP: (bool) -1 === true
  • ECDSA 曲线

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

    P-224、P-256、P-384、P-521、secp256k1 和 brainpoolP256r1/P384r1/P512r1 的签名与验证均成功。

    检查记录
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    通过 OPENSSL_KEYTYPE_ED25519 和 OPENSSL_KEYTYPE_ED448 支持 Ed25519 和 Ed448;还捆绑了 libsodium 的 sodium_crypto_sign_*。

    检查记录
    fact eddsa = Ed25519, Ed448
    evidence eddsa: OPENSSL_KEYTYPE_ED25519 defined: true; sodium_crypto_sign available: true
  • 后量子算法

    none

    扩展中没有后量子密钥类型常量,尽管镜像中的 OpenSSL CLI 列出了 ML-DSA。

    检查记录
    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)
  • 公钥格式

    spki-pemx509

    PEM 格式的 SPKI 和 X.509 证书;裸 SPKI DER 字节被拒绝。

    检查记录
    fact public-key-formats = spki-pem, x509
    evidence public-key-formats: SPKI DER bytes passed as the key: openssl_verify returned false
  • 私钥格式

    pkcs8-pemencrypted-pkcs8sec1-pem

    PKCS#8 PEM、加密的 PKCS#8 和 SEC1 EC PRIVATE KEY;openssl_pkey_export() 写出 PKCS#8。

    检查记录
    fact private-key-formats = pkcs8-pem, encrypted-pkcs8, sec1-pem
    evidence private-key-formats: openssl_pkey_export writes -----BEGIN PRIVATE KEY-----

用法

用 P-256 签名,并说明为什么 openssl_verify() 的结果必须用 === 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";

代码片段测试工具会在容器镜像 php:8.5.11-cli-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
php usage.php 2>/dev/null
输出必须匹配(正则表达式)
valid: true, raw 64 bytes gives -1, which is truthy: true

库