So What Does An Elliptic Curve Key Pair Look Like? PEM, DER and OpenSSH

Elliptic curve cryptography (ECC) saved cybersecurity a whole lot of processing. Without them, we would be implementing key exchange with…

So What Does An Elliptic Curve Key Pair Look Like? PEM, DER and OpenSSH

Elliptic curve cryptography (ECC) saved cybersecurity a whole lot of processing. Without them, we would be implementing key exchange with large prime numbers of over 2K bits. With them, our key exchanges are small, with a private key of around 256 bits and a public key of 512 bits.

An elliptic curve can have the relationship of:

=x³+ax+b (mod p)

and for a defined prime number (p). Overall, NIST has defined a number of standard curves, and which relate to the size of their finite field. These are P256, P384 and P512. With P-256 we use: p=2²⁵⁶−2²²⁴+2¹⁹²+2⁹⁶−1, a=−3, b=41058363725152142129326129780047268409114441015993725554835256314039467401291.

In ECC, we have a private key of sk and which is a scalar value. The public key is then:

pk=sk.G

and where G is the base point on the curve, and pk is a public key point. If sk has 256 bits, then pk will have 512 bits, as it is an (x,y) point.

Now, the EITF has just published RFC 9500 [here] which defined test keys for RSA, DLP (Discrete Logarithm Problem) and ECDLP (Elliptic Curve DLP) keys for testing. With DLP, we have discrete log methods such as the Diffie-Hellman method and ElGamal, and with ECDLP, we have elliptic curve methods, such as for ECDH and ECDSA.

In RFC 9500, we have the key pair defined in a hexadecimal byte array form:

For this, we have the d array as the private key, and qx and qx defining the (x,y) coordinates for the public key point. We also have a PEM format for the private key. First, let’s code this into a program and see the outputs for the private key and the public key [here]:

import sys
from Crypto.PublicKey import ECC
from Crypto.Util.number import *
from ecdsa import ECDH, NIST256p
import binascii
msg="hello"
test=1
curve_name='P-256'

if (len(sys.argv)>1):
test=int(sys.argv[1])
try:
M= bytes_to_long(msg.encode('utf-8'))
if (test==1):
d=0xE6CB5BDD80AA45AE9C95E8C15476679FFEC953C16851E711E743939589C64FC1
curve_name='P-256'
if (test==2):
d=0xE2563328DFABF68188606B91324281C1D58A4456431B09D510B35FECC9F307CA1822846FA2671371A9A81BAC0E35749D
curve_name='P-384'
if (test==3):
d=0x01D924DCCA0A887F8D99767A37D874E637A12CCB477D6E08665356694D68B7655E5069638FDE7B45C854013DC77A35B18655B84C966A60220D40F91ED9F5145802EA
curve_name='P-521'

key=ECC.construct(curve=curve_name,d=d)
print("=== ECC Private key ===")
print (f"d={hex(d)}\n")
print (f"Public Key (x) {hex(key.pointQ.x)}")
print (f"Public Key (y) {hex(key.pointQ.y)}")

print("\n=== Private Key PEM format ===")
eccKey = ECC.construct( curve=curve_name,d=d )
pubKeyPEM = eccKey.export_key(format='PEM')
print(pubKeyPEM)
print("\n=== Private Key DER format ===")
pubKeyDER = eccKey.export_key(format='DER')
print(binascii.hexlify(pubKeyDER).decode())

pub= eccKey.public_key()
print("\n=== Public Key PEM format ===")
pubKeyPEM = pub.export_key(format='PEM')
print(pubKeyPEM)
print("\n=== Public Key DER format ===")
pubKeyDER = pub.export_key(format='DER')
print(binascii.hexlify(pubKeyDER).decode())
print("\n=== Public Key OpenSSH format ===")
pubKeyOpen = pub.export_key(format='OpenSSH')
print(pubKeyOpen)

except Exception as e:
print(e)

and a sample run for P256 shows the correct public key [here]:

=== ECC Private key ===
d=0xe6cb5bdd80aa45ae9c95e8c15476679ffec953c16851e711e743939589c64fc1
Public Key (x) 0x422548f88fb782ffb5eca3744452c72a1e558fbd6f73be5e48e93232cc45c5b1
Public Key (y) 0x6c4cd10c4cb8d5b8a17139e94882c8992572993425f41419ab7e90a42a494272

=== Private Key PEM format ===
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5stb3YCqRa6clejB
VHZnn/7JU8FoUecR50OTlYnGT8GhRANCAARCJUj4j7eC/7Xso3REUscqHlWPvW9z
vl5I6TIyzEXFsWxM0QxMuNW4oXE56UiCyJklcpk0JfQUGat+kKQqSUJy
-----END PRIVATE KEY-----

=== Private Key DER format ===
308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420e6cb5bdd80aa45ae9c95e8c15476679ffec953c16851e711e743939589c64fc1a14403420004422548f88fb782ffb5eca3744452c72a1e558fbd6f73be5e48e93232cc45c5b16c4cd10c4cb8d5b8a17139e94882c8992572993425f41419ab7e90a42a494272
=== Public Key PEM format ===

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQiVI+I+3gv+17KN0RFLHKh5Vj71v
c75eSOkyMsxFxbFsTNEMTLjVuKFxOelIgsiZJXKZNCX0FBmrfpCkKklCcg==
-----END PUBLIC KEY-----

=== Public Key DER format ===
3059301306072a8648ce3d020106082a8648ce3d03010703420004422548f88fb782ffb5eca3744452c72a1e558fbd6f73be5e48e93232cc45c5b16c4cd10c4cb8d5b8a17139e94882c8992572993425f41419ab7e90a42a494272

=== Public Key OpenSSH format ===
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEIlSPiPt4L/teyjdERSxyoeVY+9b3O+XkjpMjLMRcWxbEzRDEy41bihcTnpSILImSVymTQl9BQZq36QpCpJQnI=

The key is constructed by just using the curve name and the private key (d):

key=ECC.construct(curve=curve_name,d=d)
print("=== ECC Private key ===")
print (f"d={hex(d)}\n")
print (f"Public Key (x) {hex(key.pointQ.x)}")
print (f"Public Key (y) {hex(key.pointQ.y)}")

We can see we have three forms of the private key: raw (d and (x,y)), PEM and DER. The DER format is a binary format for the key pair, while PEM defines the key in a Base 64 format. With DER, we have the 0x30 value at the start. This identifies the ANS1 tag of “SEQUENCE”. Using OpenSSL, we can parse with [here]:

echo 308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b020101042
0e6cb5bdd80aa45ae9c95e8c15476679ffec953c16851e711e743939589c64fc1a1440342000
4422548f88fb782ffb5eca3744452c72a1e558fbd6f73be5e48e93232cc45c5b16c4cd10c4cb
8d5b8a17139e94882c8992572993425f41419ab7e90a42a494272
| xxd -r -p | openssl asn1parse -inform der

0:d=0 hl=3 l= 135 cons: SEQUENCE
3:d=1 hl=2 l= 1 prim: INTEGER :00
6:d=1 hl=2 l= 19 cons: SEQUENCE
8:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
17:d=2 hl=2 l= 8 prim: OBJECT :prime256v1
27:d=1 hl=2 l= 109 prim: OCTET STRING
0000 - 30 6b 02 01 01 04 20 e6-cb 5b dd 80 aa 45 ae 9c 0k.... ..[...E..
0010 - 95 e8 c1 54 76 67 9f fe-c9 53 c1 68 51 e7 11 e7 ...Tvg...S.hQ...
0020 - 43 93 95 89 c6 4f c1 a1-44 03 42 00 04 42 25 48 C....O..D.B..B%H
0030 - f8 8f b7 82 ff b5 ec a3-74 44 52 c7 2a 1e 55 8f ........tDR.*.U.
0040 - bd 6f 73 be 5e 48 e9 32-32 cc 45 c5 b1 6c 4c d1 .os.^H.22.E..lL.
0050 - 0c 4c b8 d5 b8 a1 71 39-e9 48 82 c8 99 25 72 99 .L....q9.H...%r.
0060 - 34 25 f4 14 19 ab 7e 90-a4 2a 49 42 72 4%....~..*IBr

We can see that the key is also defined in a DER format, so we can parse it with [here]:

echo 306b0201010420e6cb5bdd80aa45ae9c95e8c15476679ffec953c16851e711e74393958
9c64fc1a14403420004422548f88fb782ffb5eca3744452c72a1e558fbd6f73be5e48e93232c
c45c5b16c4cd10c4cb8d5b8a17139e94882c8992572993425f41419ab7e90a42a494272
| xxd -r -p | openssl asn1parse -inform der

0:d=0 hl=2 l= 107 cons: SEQUENCE
2:d=1 hl=2 l= 1 prim: INTEGER :01
5:d=1 hl=2 l= 32 prim: OCTET STRING
0000 - e6 cb 5b dd 80 aa 45 ae-9c 95 e8 c1 54 76 67 9f ..[...E.....Tvg.
0010 - fe c9 53 c1 68 51 e7 11-e7 43 93 95 89 c6 4f c1 ..S.hQ...C....O.
39:d=1 hl=2 l= 68 cons: cont [ 1 ]
41:d=2 hl=2 l= 66 prim: BIT STRING
0000 - 00 04 42 25 48 f8 8f b7-82 ff b5 ec a3 74 44 52 ..B%H........tDR
0010 - c7 2a 1e 55 8f bd 6f 73-be 5e 48 e9 32 32 cc 45 .*.U..os.^H.22.E
0020 - c5 b1 6c 4c d1 0c 4c b8-d5 b8 a1 71 39 e9 48 82 ..lL..L....q9.H.
0030 - c8 99 25 72 99 34 25 f4-14 19 ab 7e 90 a4 2a 49 ..%r.4%....~..*I
0040 - 42 72 Br

As we can see this reveals the correct private key (0xe6cb …) and the public key has a “00 04" in front of it. Overall, the “00” identifies a public key and “04” identifies that it is an uncompressed public key, and thus contains an (x,y) point.

With PEM, we can easily transport the keys over a text-based message. We output the PEM and DER formats for the private key with:

 pubKeyPEM = eccKey.export_key(format='PEM')
print(pubKeyPEM)
print("\n=== Private Key DER format ===")
pubKeyDER = eccKey.export_key(format='DER')
print(binascii.hexlify(pubKeyDER).decode())

Note that the private key contains both the private key (sk) and the public key (sk.G), and will thus be larger than the exported public key. For the public key, we export the public key and then display it in PEM, DER and OpenSSH format:

 pub= eccKey.public_key()
print("\n=== Public Key PEM format ===")
pubKeyPEM = pub.export_key(format='PEM')
print(pubKeyPEM)
print("\n=== Public Key DER format ===")
pubKeyDER = pub.export_key(format='DER')
print(binascii.hexlify(pubKeyDER).decode())
print("\n=== Public Key OpenSSH format ===")
pubKeyOpen = pub.export_key(format='OpenSSH')
print(pubKeyOpen)

With OpenSSH, we have the form of:

=== Public Key OpenSSH format ===
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEIlS
PiPt4L/teyjdERSxyoeVY+9b3O+XkjpMjLMRcWxbEzRDEy41bihcTnpSILImSVymTQl9BQZq36QpC
pJQnI=

This form is typically used to remotely log into a system with SSH. With P-384, the get:

=== ECC Private key ===
d=0xe2563328dfabf68188606b91324281c1d58a4456431b09d510b35fecc9f307ca1822846fa2671371a9a81bac0e35749d
Public Key (x) 0x5b0901b88523296eb919d50ffa1a9cb374bc4d409586282bfeca11b1d95adbb54734af570bf82b7228cf226bcf4c25dd
Public Key (y) 0xbcfe3b1a3ad39430eff763e1d68d2e151d91720b7795b58da6b34639613a8fb9b5a8da48c6747117f9919e8424f37ec8
=== Private Key PEM format ===
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDiVjMo36v2gYhga5Ey
QoHB1YpEVkMbCdUQs1/syfMHyhgihG+iZxNxqagbrA41dJ2hZANiAARbCQG4hSMp
brkZ1Q/6GpyzdLxNQJWGKCv+yhGx2VrbtUc0r1cL+CtyKM8ia89MJd28/jsaOtOU
MO/3Y+HWjS4VHZFyC3eVtY2ms0Y5YTqPubWo2kjGdHEX+ZGehCTzfsg=
-----END PRIVATE KEY-----
=== Private Key DER format ===
3081b6020100301006072a8648ce3d020106052b8104002204819e30819b0201010430e2563328dfabf68188606b91324281c1d58a4456431b09d510b35fecc9f307ca1822846fa2671371a9a81bac0e35749da164036200045b0901b88523296eb919d50ffa1a9cb374bc4d409586282bfeca11b1d95adbb54734af570bf82b7228cf226bcf4c25ddbcfe3b1a3ad39430eff763e1d68d2e151d91720b7795b58da6b34639613a8fb9b5a8da48c6747117f9919e8424f37ec8
=== Public Key PEM format ===
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWwkBuIUjKW65GdUP+hqcs3S8TUCVhigr
/soRsdla27VHNK9XC/grcijPImvPTCXdvP47GjrTlDDv92Ph1o0uFR2Rcgt3lbWN
prNGOWE6j7m1qNpIxnRxF/mRnoQk837I
-----END PUBLIC KEY-----
=== Public Key DER format ===
3076301006072a8648ce3d020106052b81040022036200045b0901b88523296eb919d50ffa1a9cb374bc4d409586282bfeca11b1d95adbb54734af570bf82b7228cf226bcf4c25ddbcfe3b1a3ad39430eff763e1d68d2e151d91720b7795b58da6b34639613a8fb9b5a8da48c6747117f9919e8424f37ec8
=== Public Key OpenSSH format ===
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBFsJAbiFIyluuRnVD/oanLN0vE1AlYYoK/7KEbHZWtu1RzSvVwv4K3IozyJrz0wl3bz+Oxo605Qw7/dj4daNLhUdkXILd5W1jaazRjlhOo+5tajaSMZ0cRf5kZ6EJPN+yA==

And P-512:

=== ECC Private key ===
d=0x1d924dcca0a887f8d99767a37d874e637a12ccb477d6e08665356694d68b7655e5069638fde7b45c854013dc77a35b18655b84c966a60220d40f91ed9f5145802ea
Public Key (x) 0x1d0fd7257a84c747f562575c07385dbebf2f52bea58083db82fdd1531d8aae3cc875ff02ff7fa2da260d8eb62d6d2f5d649278e321736a0628cbbb30308b6e618db
Public Key (y) 0xf62ad204c6460359bc818ab8961bf0f0fc0ec5aae8a428173ce56f00de9b157c1e5c82c64f562fcadefc4a4c28f6d342cf3ef616fc82d33b7285c921f2bf36fdd8
=== Private Key PEM format ===
-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB2STcygqIf42Zdno3
2HTmN6Esy0d9bghmU1ZpTWi3ZV5QaWOP3ntFyFQBPcd6NbGGVbhMlmpgIg1A+R7Z
9RRYAuqhgYkDgYYABAHQ/XJXqEx0f1YldcBzhdvr8vUr6lgIPbgv3RUx2KrjzIdf
8C/3+i2iYNjrYtbS9dZJJ44yFzagYoy7swMItuYY2wD2KtIExkYDWbyBiriWG/Dw
/A7FquikKBc85W8A3psVfB5cgsZPVi/K3vxKTCj200LPPvYW/ILTO3KFySHyvzb9
2A==
-----END PRIVATE KEY-----
=== Private Key DER format ===
3081ee020100301006072a8648ce3d020106052b810400230481d63081d3020101044201d924dcca0a887f8d99767a37d874e637a12ccb477d6e08665356694d68b7655e5069638fde7b45c854013dc77a35b18655b84c966a60220d40f91ed9f5145802eaa18189038186000401d0fd7257a84c747f562575c07385dbebf2f52bea58083db82fdd1531d8aae3cc875ff02ff7fa2da260d8eb62d6d2f5d649278e321736a0628cbbb30308b6e618db00f62ad204c6460359bc818ab8961bf0f0fc0ec5aae8a428173ce56f00de9b157c1e5c82c64f562fcadefc4a4c28f6d342cf3ef616fc82d33b7285c921f2bf36fdd8
=== Public Key PEM format ===
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB0P1yV6hMdH9WJXXAc4Xb6/L1K+pY
CD24L90VMdiq48yHX/Av9/otomDY62LW0vXWSSeOMhc2oGKMu7MDCLbmGNsA9irS
BMZGA1m8gYq4lhvw8PwOxaropCgXPOVvAN6bFXweXILGT1Yvyt78Skwo9tNCzz72
FvyC0ztyhckh8r82/dg=
-----END PUBLIC KEY-----
=== Public Key DER format ===
30819b301006072a8648ce3d020106052b81040023038186000401d0fd7257a84c747f562575c07385dbebf2f52bea58083db82fdd1531d8aae3cc875ff02ff7fa2da260d8eb62d6d2f5d649278e321736a0628cbbb30308b6e618db00f62ad204c6460359bc818ab8961bf0f0fc0ec5aae8a428173ce56f00de9b157c1e5c82c64f562fcadefc4a4c28f6d342cf3ef616fc82d33b7285c921f2bf36fdd8
=== Public Key OpenSSH format ===
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAHQ/XJXqEx0f1YldcBzhdvr8vUr6lgIPbgv3RUx2KrjzIdf8C/3+i2iYNjrYtbS9dZJJ44yFzagYoy7swMItuYY2wD2KtIExkYDWbyBiriWG/Dw/A7FquikKBc85W8A3psVfB5cgsZPVi/K3vxKTCj200LPPvYW/ILTO3KFySHyvzb92A==

Conclusions

ECC is wonderful, and so efficient for processing, and you can thank it every single time you connect to the Internet. Without the usage of ECDH (Elliptic Curve Diffie Hellman) key exchange, our browsers would have to do a whole lot more work, and it would all be a whole lot slower to create a new connection.