Passing public key in PEM format to openssl_pkey_get_public gives error:0906D06C:PEM routines:PEM_read_bio:no start line

Passing public key in PEM format to openssl_pkey_get_public gives error:0906D06C:PEM routines:PEM_read_bio:no start line

以下 public PEM 格式的 RSA 密钥已提供给 openssl_pkey_get_public。

-----BEGIN PUBLIC KEY-----
MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQCIZouo/rL5IkIIGrke/qkY
Nsb9JDXUw2MfutYdwIVjPiEbAcLiVxK6tOVXy7dq+hU0zyNd68bUi7VJjXWoiepS
+Mm6v76GCGvVvno48m7ofWIq6VLEaMQjIM/pzkF0TW7CmtjKvgg722Hx87AI/KCM
sWuHjhcQZsMgV4ibC8EAY6GYwHYAPWfUq+LI2wfRsQHumFC2IuT4guO/Vs5FJGXw
Arrvv7VPyKwZ8cpcZn9ka1K0N7su7QiGnzOhS3n2THaj25alE6TMXnrKmt6yIiXh
amsKVEKPPzHpw9ldTao1aG7vVNC9QXC8i9uQTWhhokxvSNw5OYFFkDZC5jD7McvB
AgMBAAE=
-----END PUBLIC KEY-----

然而,方法调用失败,返回 false,错误字符串 error:0906D06C:PEM routines:PEM_read_bio:no start line

public密钥是否无效?作为记录,我的代码以 public 密钥模数和指数开始,并使用 here.

发布的算法将其转换为 PEM 格式

完整脚本如下:

<?php

function createPemFromModulusAndExponent($n, $e)
{
    $modulus = urlsafeB64Decode($n);
    $publicExponent = urlsafeB64Decode($e);
    $components = array(
        'modulus' => pack('Ca*a*', 2, encodeLength(strlen($modulus)), $modulus),
        'publicExponent' => pack('Ca*a*', 2, encodeLength(strlen($publicExponent)), $publicExponent)
    );

    $RSAPublicKey = pack('Ca*a*a*', 48, encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']);

    $rsaOID = pack('H*', '300d06092a864886f70d0101010500');
    $RSAPublicKey = chr(0) . $RSAPublicKey;
    $RSAPublicKey = chr(3) . encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey;
    $RSAPublicKey = pack('Ca*a*', 48, encodeLength(strlen($rsaOID . $RSAPublicKey)), $rsaOID . $RSAPublicKey);

    $RSAPublicKey = "-----BEGIN PUBLIC KEY-----" . chunk_split(base64_encode($RSAPublicKey), 64) . '-----END PUBLIC KEY-----';
    return $RSAPublicKey;
}

function urlsafeB64Decode($input)
{
    $remainder = strlen($input) % 4;
    if ($remainder)
    {
        $padlen = 4 - $remainder;
        $input .= str_repeat('=', $padlen);
    }
    return base64_decode(strtr($input, '-_', '+/'));
}

function encodeLength($length)
{
    if ($length <= 0x7F)
    {
        return chr($length);
    }

    $temp = ltrim(pack('N', $length), chr(0));
    return pack('Ca*', 0x80 | strlen($temp), $temp);
}

$key = createPemFromModulusAndExponent('iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ', 'AQAB');

print_r($key);

print_r(openssl_pkey_get_public($key));

print_r(openssl_error_string());

首先:openssl_pkey_get_public 旨在直接 加载 中提取 证书,如 openssl_pkey_get_public.

certificate 参数的文档中所述

已经针对此问题提交了错误,#75643 from Dec 2017 (version 7.1.12), which has the status No Feedback and is currently suspended (note that #75643 actually refers to openssl_public_encrypt, which however uses the same logic regarding the key as openssl_pkey_get_public, here):

The error in the queue is expected. If you supply string as a PEM (string not prefixed by "file://" which would be a file path), then certificate is tried first (using PEM_ASN1_read_bio). It means that it fails and the error is saved to the queue. However this queue is just a copy of the OpenSSL which is emptied. After that the key is loaded using PEM_read_bio_PUBKEY which is successful in your case so you get back the result. To sum it up openssl_error_string does not mean that the operation failed but just that some error was emitted...

据此,错误信息是由于无法从证书中提取密钥导致的。但是,继续处理并直接加载密钥。换句话说,当直接直接加载密钥时,错误消息会按预期出现,并且在此上下文中可以忽略(至少在直接加载成功的情况下)。

记录:自 7.2(.17) 起,显示的错误消息略有不同:error:0909006C:PEM routines:get_name:no start line


更新:

正如@President James Moveon Polk 在他的评论中指出的那样,createPemFromModulusAndExponent 没有正确生成密钥。如果第一个/最高有效字节大于 0x7F,则模数前面必须有一个 0x00 字节,目前不会发生这种情况。例如。在发布的代码中,模数以 0x88 开头(Base64url 解码),这意味着生成的(= 发布的)密钥无效。如果手动添加 0x00 并将如此更正的值(Base64url 编码)传递给 createPemFromModulusAndExponent,则以下内容现在 有效 关键结果:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiGaLqP6y+SJCCBq5Hv6p
GDbG/SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInq
UvjJur++hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPyg
jLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk+ILjv1bORSRl
8AK677+1T8isGfHKXGZ/ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl
4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw+zHL
wQIDAQAB
-----END PUBLIC KEY-----

当然如果createPemFromModulusAndExponent能自动修正就更好了。 @President James Moveon Polk 已为此提出问题,here

请允许我提出一种更简单、更简洁的替代方法。使用 phpseclib,

require __DIR__ . '/vendor/autoload.php';

use phpseclib\Math\BigInteger;
use phpseclib\Crypt\RSA;

$rsa = new RSA;
$rsa->loadKey([
    'e' => new BigInteger(base64_decode('AQAB'), 256),
    'n' => new BigInteger(base64_decode('iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ'), 256)
]);

print_r(openssl_pkey_get_public($rsa));

您使用的代码实际上是使用从 phpseclib 2.0 中提取的代码。有关详细信息,请参阅 https://github.com/dragosgaftoneanu/okta-simple-jwt-verifier/issues/1#issuecomment-612503921