Wirone
Wirone

Reputation: 3373

PHP: Convert DER private key to PEM format (Elliptic Curve)

I have test keys generated like here:

openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem
openssl ec -in vapid_private.pem -pubout -out vapid_public.pem
openssl ec -in vapid_private.pem -pubout -outform DER | tail -c 65 | base64 | tr -d '=' | tr '/+' '_-' > vapid_public.der
openssl ec -in vapid_private.pem -outform DER | tail -c +8 | head -c 32 | base64 | tr -d '=' | tr '/+' '_-' > vapid_private.der

vapid_private.pem:

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICBP2ZDHFWjIfnJXDCXFMIQIXRwQofx531ikc2R3RK+qoAoGCCqGSM49
AwEHoUQDQgAEtHLtsp1L1Jaqj/IEueEvp9yvG3wg1POAlzEf77NsM9qTMFBQiBPj
Bu8tzN5lLXnjKMy4I+AATyOI7Kz2C8RVWg==
-----END EC PRIVATE KEY-----

vapid_public.pem:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtHLtsp1L1Jaqj/IEueEvp9yvG3wg
1POAlzEf77NsM9qTMFBQiBPjBu8tzN5lLXnjKMy4I+AATyOI7Kz2C8RVWg==
-----END PUBLIC KEY-----

vapid_private.pem:

IE_ZkMcVaMh-clcMJcUwhAhdHBCh_HnfWKRzZHdEr6o

vapid_public.pem:

BLRy7bKdS9SWqo_yBLnhL6fcrxt8INTzgJcxH--zbDPakzBQUIgT4wbvLczeZS154yjMuCPgAE8jiOys9gvEVVo

Then I have converter class that handles conversion. 3 out of 4 conversions work properly, I can't get private DER→PEM to work. This is minified (stripped error handling etc.) function that does it:

function convertECPrivateKeyFromDERtoPEM(string $der, string $passphrase = null): string
{
    $fullDer = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42\x00" . self::base64_decode_url($der);
    return "-----BEGIN EC PRIVATE KEY-----\n" . chunk_split(base64_encode($fullDer), 64, "\n") . "-----END EC PRIVATE KEY-----";
}

This approach works properly for public key (of course with different header+footer), it was implemented earlier by other developer. I need to add support for private keys and I struggle to get proper PEM key. Mentioned function returns PEM-like key, but it's not valid Elliptic Curve key - it can't be loaded with openssl_pkey_get_private().

I've looked at some libraries, but:

The exact same key pair can be properly converted using PERL:

perl -E 'use Crypt::PK::ECC; use MIME::Base64;my $k = Crypt::PK::ECC->new->import_key_raw(MIME::Base64::decode_base64url("IE_ZkMcVaMh-clcMJcUwhAhdHBCh_HnfWKRzZHdEr6o"), "prime256v1"); say $k->export_key_pem("private_short")'

and it's possible to get exact same PEM key like the one from which DER was generated, so it definitely is possible, but I can't get PHP to do it.

Errors from OpenSSL:

error:0D07209B:asn1 encoding routines:ASN1_get_object:too long
error:0D068066:asn1 encoding routines:asn1_check_tlen:bad object header
error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error
error:10092010:elliptic curve routines:d2i_ECPrivateKey:EC lib
error:100DE08E:elliptic curve routines:old_ec_priv_decode:decode error
error:0D07209B:asn1 encoding routines:ASN1_get_object:too long
error:0D068066:asn1 encoding routines:asn1_check_tlen:bad object header
error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error
error:0907B00D:PEM routines:PEM_read_bio_PrivateKey:ASN1 lib

I'm not so much into cryptography so I find it hard to get solution. Thanks in advance for any help!

Upvotes: 2

Views: 1099

Answers (1)

Topaco
Topaco

Reputation: 49571

The last two posted keys are not DER encoded data but the Base64url encoded raw private key and the Base64url encoded uncompressed public key (which is the concatenation of 0x04|<x>|<y>).

The conversion of the raw to the PEM keys is possible with e.g. mdanter/ecc:

use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;

$nist = EccFactory::getNistCurves();
$generator = $nist->generator256(); // prime256v1
$adapter = EccFactory::getAdapter();

// Create SEC1-PEM from raw private key
$rawPrivKey = gmp_import(base64_decode_url("IE_ZkMcVaMh-clcMJcUwhAhdHBCh_HnfWKRzZHdEr6o"));
$privKey = $generator->getPrivateKeyFrom($rawPrivKey);
$derPrivKeySerializer = new DerPrivateKeySerializer($adapter);
$pemPrivKesSerializer = new PemPrivateKeySerializer($derPrivKeySerializer);
$privKeyPem = $pemPrivKesSerializer->serialize($privKey);
print($privKeyPem . PHP_EOL);

// Create X.509-PEM from uncompressed public key
$uncompressedPubKey = base64_decode_url("BLRy7bKdS9SWqo_yBLnhL6fcrxt8INTzgJcxH--zbDPakzBQUIgT4wbvLczeZS154yjMuCPgAE8jiOys9gvEVVo");
$x = gmp_import(substr($uncompressedPubKey, 1, 32));
$y = gmp_import(substr($uncompressedPubKey,33, 32));
$pubKey = $generator->getPublicKeyFrom($x, $y);
$derPubKeySerializer = new DerPublicKeySerializer($adapter);
$pemPubKeySerializer = new PemPublicKeySerializer($derPubKeySerializer);
$pubKeyPem = $pemPubKeySerializer->serialize($pubKey);
print($pubKeyPem . PHP_EOL);

// Helper
function base64_decode_url( $data ){
    return base64_decode( strtr( $data, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $data )) % 4 ));
}

Both PEM keys match the posted PEM keys.


Another approach is to create the ASN.1/DER encoding yourself, exploiting that for the same curve certain byte sequences are identical and only the modified data needs to be replaced, i.e. the raw private key and the uncompressed public key. This seems to be the approach you used to convert the public key. In this post you will find a detailed description of this approach and the various byte sequences, especially for prime256v1 at the end of the post. In the following this is shown for the private PEM key:

function convertFromRawToSec1PEM(string $rawPrivate, string $uncompressedPublic, string $passphrase = null): string
{
    $fullDer = hex2bin("30770201010420") . base64_decode_url($rawPrivate) . hex2bin("a00a06082a8648ce3d030107a144034200") . base64_decode_url($uncompressedPublic);
    return "-----BEGIN EC PRIVATE KEY-----\n" . chunk_split(base64_encode($fullDer), 64, "\n") . "-----END EC PRIVATE KEY-----";
}

$rawPrivate = "IE_ZkMcVaMh-clcMJcUwhAhdHBCh_HnfWKRzZHdEr6o";
$uncompressedPublic = "BLRy7bKdS9SWqo_yBLnhL6fcrxt8INTzgJcxH--zbDPakzBQUIgT4wbvLczeZS154yjMuCPgAE8jiOys9gvEVVo";
$sec1Pem = convertFromRawToSec1PEM($rawPrivate, $uncompressedPublic);
print($sec1Pem . PHP_EOL);

The private PEM key matches the posted private PEM key.


The first approach is generic and can be quickly adapted for other curves. However, this costs the dependence on a library.
The second approach does not require a library, can be implemented more compactly, but is more elaborate to prepare because the byte sequences must be determined (explicitly or using a known key of the same curve), into which the raw keys must be integrated at the correct positions.

Upvotes: 2

Related Questions