2

The following public RSA key in PEM format was provided to openssl_pkey_get_public.

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

However, the method call fails, returning false, with the error string error:0906D06C:PEM routines:PEM_read_bio:no start line

Is the public key invalid? For the record, my code is starting with a public key modulus and exponent and converting it to PEM format using the algorithm posted here.

Here's the full script:

<?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());
rurouniwallace
  • 2,027
  • 6
  • 25
  • 47
  • Check the exponent: `ABAQ` is Base64 decoded `001010`, which is decimal `4112`. Isn't `AQAB` more likely, which is Base64 decoded `010001` and decimal `65537`, a typical value for the exponent? – Topaco Apr 04 '20 at 18:49
  • @topaco Not only is it more likely, it's true. Thanks for catching that. Changing it to `AQAB` still gives the same error though. I'll update the question with the correct exponent and PEM. – rurouniwallace Apr 04 '20 at 19:33
  • Actually, [`openssl_pkey_get_public`](https://www.php.net/manual/en/function.openssl-pkey-get-public.php) is used to extract a public key from a certificate, the function is not _directly_ applied to a public key. The error probably disappears when the key is read from a certificate. Despite the error, it is quite possible that the operation (e.g. the verification of a signature) will work anyway, see [here](https://stackoverflow.com/a/3635877/9014097). – Topaco Apr 04 '20 at 20:24
  • 1
    Confirmed: 1) No error message is displayed if the public key is extracted from a certificate. 2) The error message does not influence the result of the operation (this applies at least to the verification of a signature). – Topaco Apr 04 '20 at 22:47
  • @Topaco write that as an answer and I'll accept. Thanks! – rurouniwallace Apr 04 '20 at 23:35
  • 1
    @Topaco: Not according the documentation. It can read exactly the key presented by op, in fact this very example worked fine for me with PHP 7.4.4 – President James K. Polk Apr 04 '20 at 23:56
  • The issue is already filed [#75643](https://bugs.php.net/bug.php?id=75643), see my answer. – Topaco Apr 05 '20 at 11:23
  • 1
    @PresidentJamesMoveonPolk - I can reproduce the bug for 7.4.4 as well. Maybe a misunderstanding: The error message _only_ occurs when `openssl_error_string()` is explicitly displayed. Otherwise, no error message is displayed. Independent of this, the operation is executed successfully. Regarding the documentation: Yes, according to the description of the certificate parameter, a public key can also be loaded _directly_, i.e. without a certificate. Here I was misled by the abstract (_Extract public key from certificate..._) and the error message. – Topaco Apr 05 '20 at 11:24
  • 2
    @Topaco: Ah, I see. Yes, the error message is present, you're correct. And the key can be used to encrypt. But I discovered another problem with the key: it is *not* valid because the modulus is negative! I guess `openssl_pkey_get_public()` just ignores negative moduli and treats them as positive. It does mean that whatever code produced the PEM encoded key is buggy. – President James K. Polk Apr 05 '20 at 12:46
  • 2
    I opened an [issue](https://github.com/dragosgaftoneanu/okta-simple-jwt-verifier/issues/1) about this bug. – President James K. Polk Apr 05 '20 at 13:25
  • 2
    @PresidentJamesMoveonPolk - Yes, you're right. A possible Workaround: Since `createPemFromModulusAndExponent` expects the modulus as (Base64Url encoded) hex string, it can be passed in the already correct notation. Then a valid key will be generated. Of course, the better way would be the fix of `createPemFromModulusAndExponent`. – Topaco Apr 05 '20 at 18:39

2 Answers2

4

First: openssl_pkey_get_public is intended to either load the public key directly or extract it from a certificate, as described in the documentation of the certificate parameter of openssl_pkey_get_public.

There has already been a bug filed for this issue, #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...

According to this, the error message is caused by the failure to extract the key from the certificate. However, processing is continued and the key is loaded directly. In other words, the error message occurs as expected when loading the key directly and can be ignored in this context (at least if the direct loading is successful).

For the records: As of 7.2(.17), a slightly different error message is displayed: error:0909006C:PEM routines:get_name:no start line.


Update:

As @President James Moveon Polk noted in his comment, createPemFromModulusAndExponent doesn't generate the key correctly. If the first / most significant byte is greater than 0x7F, the modulus must be preceded by a 0x00 byte, which does currently not happen. E.g. in the posted code the modulus starts (Base64url decoded) with 0x88, which means that the generated (= the posted) key is invalid. If a 0x00 is prepended manually and the so corrected value is (Base64url encoded) passed to createPemFromModulusAndExponent, the following, now valid key results:

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

Of course it would be better if createPemFromModulusAndExponent would do this correction automatically. @President James Moveon Polk has filed an issue for this, here.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Can you show the code you used to prepend the 0 byte to the modulus? I added the line `$modulus = chr(0x00) . $modulus;` to the top of `createPemFromModulusAndExponent` and got a different PEM key. – rurouniwallace Apr 06 '20 at 16:30
  • This is right, but must be performed _after_ the Base64 decoding of the modulus, i.e. _after_ the `urlsafeB64Decode($n)`-call. – Topaco Apr 06 '20 at 17:16
-1

Allow me to propose an alternative way that's quite a bit simpler and more succinct. Using 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));

The code you're using is, in fact, using code that was lifted from phpseclib 2.0. See https://github.com/dragosgaftoneanu/okta-simple-jwt-verifier/issues/1#issuecomment-612503921 for more info.

neubert
  • 15,947
  • 24
  • 120
  • 212