JSON形式の公開鍵をPEM形式に変換方法(PHP)

schedule 2020/07/29  refresh 2023/11/09

 

 

JSON形式の公開鍵をPEM形式に変換する方法をご紹介させて頂きます。

 

OpenID Connectの署名検証によく使われる証明書公開鍵のJSONフォーマットでは exponent(e)と modulus(n)として表されます。


SeciossLinkの場合こちらでご確認頂けます。

 

{
    "keys": [
        {
            "kty": "RSA",
            "alg": "RS256",
            "use": "sig",
            "kid": "bea608b769586866dacb65405a0522f8",
            "n": "sV4L1Ujt5N4nt1g1taaZpk-R5woC46vZej3wDe0D6WLdhs-UWd3AaG75X5qdNnThd1zpeoF2tp8p-TMeY6KpQxcAIXeGyvs0ZUiG8XivNYdYH8DoWlZt7qvPtkYbCM9_iyFZCRw3t8y_OePMzeYAQA4Of_Ygn4RKt9Oz-ty9HF0chDMA5GpsQWq3CPk5asKQ8rXYfYVWn6Fy6QKIrsQLhje7tUytp3DEOIAxAPQ19qNJ135wwADL_XzeXoDanBEfJQfYeW82UAIobfn6nFqgwUZCUpo6pJYv09SRCYUBpr08FhIwziom22yAwGS8ff-BK847H0VeoZB2n7OscaXKVw",
            "e": "AQAB"
        }
    ]
}

 

RHEL7 PHPで実装される場合、pem から JSON(exponent & modulus)へ

 

$key = new Crypt_RSA();
$rsa->loadKey(trim(file_get_contents('PublicKey.pem')));
$n = urlEncode($rsa->modulus->toBytes());
$e = urlEncode($rsa->publicExponent->toBytes());

 

JSON(exponent & modulus)から pem へ

 

$key = new Crypt_RSA();
$key->loadKey([
    'e' => new Math_BigInteger(urlDecode($key['e']), 256),
    'n' => new Math_BigInteger(urlDecode($key['n']), 256),
]);

 

すごく簡単ですが、php-phpseclib-crypt-rsa が RHEL8 に非対応なので、OpenSSL関数に置き換える。

 

pem から JSON(exponent & modulus)へ

 

$key= openssl_pkey_get_details(openssl_pkey_get_public(trim(file_get_contents('PublicKey.pem'))));
$n = urlEncode($key['rsa']['n']);
$e = urlEncode($key['rsa']['e']);
 

JSON(exponent & modulus)から pem へ の変換は、少し複雑になりました。

 

まずは、RHEL7と同じように、exponent & modulus を計算します

 

$exponent = bcdechex(new Math_BigInteger(urlDecode($key['e']), 256));
$modulus = bcdechex(new Math_BigInteger(urlDecode($key['n']), 256));

public static function bchexdec($hex) {
    if (strlen($hex) == 1) {
        return hexdec($hex);
    } else {
        $remain = substr($hex, 0, -1);
        $last = substr($hex, -1);
        return bcadd(bcmul(16, bchexdec($remain)), hexdec($last));
    }
}
public static function bcdechex($dec) {
    $last = bcmod($dec, 16);
    $remain = bcdiv(bcsub($dec, $last), 16);
    if ($remain == 0) {
        return dechex($last);
    } else {
        return bcdechex($remain).dechex($last);
    }

 

そうしたら、計算結果のexponent & modulusを次のファイル(def.asn1)に置き換えて保存

 
 

# Start with a SEQUENCE
asn1=SEQUENCE:pubkeyinfo
# pubkeyinfo contains an algorithm identifier and the public key wrapped
# in a BIT STRING
[pubkeyinfo]
algorithm=SEQUENCE:rsa_alg
pubkey=BITWRAP,SEQUENCE:rsapubkey
# algorithm ID for RSA is just an OID and a NULL
[rsa_alg]
algorithm=OID:rsaEncryption
parameter=NULL
# Actual public key: modulus and exponent
[rsapubkey]
n=INTEGER:0x$modulus
e=INTEGER:0x$exponent

 

そして、def.asn1を基にDERファイルを生成

 

openssl asn1parse -genconf /tmp/def.asn1 -out /tmp/pubkey.der -noout

最後は、DERからPEMへ変換

 
openssl rsa -in /tmp/pubkey.der -inform der -pubin -out /tmp/pubkey.pem

上記の手順をプログラムかすると

 
exec(echo -e "asn1=SEQUENCE:pubkeyinfo\n[pubkeyinfo]\nalgorithm=SEQUENCE:rsa_alg\npubkey=BITWRAP,SEQUENCE:rsapubkey\n[rsa_alg]\nalgorithm=OID:rsaEncryption\nparameter=NULL\n[rsapubkey]\nn=INTEGER:0x$modulus\ne=INTEGER:0x$exponent" | openssl asn1parse -genconf /dev/stdin -noout -out /dev/stdout | base64)

DERとPEMのASN.1 key構造について、こちらでご参照ください。

 
 
 

 

   0 30  159: SEQUENCE {
   3 30   13:   SEQUENCE {
   5 06    9:     OBJECT IDENTIFIER '1 2 840 113549 1 1 1'
  16 05    0:     NULL
            :     }
  18 03  141:   BIT STRING 0 unused bits, encapsulates {
  22 30  137:       SEQUENCE {
  25 02  129:         INTEGER
            :           00 EB 11 E7 B4 46 2E 09 BB 3F 90 7E 25 98 BA 2F
            :           C4 F5 41 92 5D AB BF D8 FF 0B 8E 74 C3 F1 5E 14
            :           9E 7F B6 14 06 55 18 4D E4 2F 6D DB CD EA 14 2D
            :           8B F8 3D E9 5E 07 78 1F 98 98 83 24 E2 94 DC DB
            :           39 2F 82 89 01 45 07 8C 5C 03 79 BB 74 34 FF AC
            :           04 AD 15 29 E4 C0 4C BD 98 AF F4 B7 6D 3F F1 87
            :           2F B5 C6 D8 F8 46 47 55 ED F5 71 4E 7E 7A 2D BE
            :           2E 75 49 F0 BB 12 B8 57 96 F9 3D D3 8A 8F FF 97
            :           73
 157 02    3:         INTEGER 65537
            :         }
            :       }
            :   }

 

 

 

---------------------------------------- 分 割 線 ----------------------------------------

 

やはり面倒ですね。。。
それでは、次の方法でやりましょうか

 
    ......
    $key = $this->getKeys($kid);
    $pem = $this->buildPemKey($key['e'], $key['n']);
    $key_or_secret = openssl_pkey_get_public(trim($pem));
    $valid = $jws->verify($key_or_secret);
    ......

    public function buildPemKey($e, $n)
    {
        $alg = pack("H*", "300D06092A864886F70D0101010500");
        $exp = $this->_makeAsnSegment(0x02, Akita_JOSE_Base64::urlDecode($e));
        $mod = $this->_makeAsnSegment(0x02, Akita_JOSE_Base64::urlDecode($n));
        $seq = $this->_makeAsnSegment(0x30, $mod.$exp);
        $bit = $this->_makeAsnSegment(0x03, $seq);
        $seg = $this->_makeAsnSegment(0x30, $alg.$bit);
        $b64 = base64_encode($seg);
        $res = "-----BEGIN PUBLIC KEY-----\n";
        $offset = 0;
        while ($tmp = substr($b64, $offset, 64)) {
            $res = $res.$tmp."\n";
            $offset += 64;
        }
        $res = $res."-----END PUBLIC KEY-----\n";
        $publicKey = openssl_pkey_get_public($res);
        $pkey = openssl_pkey_get_details($publicKey);
        return $res;
    }

    function _makeAsnSegment($type, $string)
    {
        switch ($type) {
            case 0x02:
                if (ord($string) > 0x7f) {
                    $string = chr(0).$string;
                }
                break;
            case 0x03:
                $string = chr(0).$string;
                break;
        }
        $length = strlen($string);
        if ($length < 128) {
            $output = sprintf("%c%c%s", $type, $length, $string);
        } elseif ($length < 0x0100) {
            $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string);
        } elseif ($length < 0x010000) {
            $output = sprintf("%c%c%c%c%s", $type, 0x82, $length/0x0100, $length%0x0100, $string);
        } else {
            $output = null;
        }
        return($output);
    }
 


以上です。