I try to generate an ECDH P-256 key pair as required for web push messages.

openssl ecparam -name prime256v1 -genkey -noout -out priv_key.pem

... generates the keys.

openssl ec -noout -text -in priv_key.pem

... looks good: As expected, the private key has 33 bytes, the public key 65. When I export the public key:

openssl ec -in priv_key.pem -pubout -out pub_key.pem

... I expect the base64 output to be 88 characters long - but what I get is 126. The server doesn't accept this key. I read about compressed base64 (https://security.stackexchange.com/a/146515/173609) but this doesn't solve the problem either (it results in an 80 byte base64 string). Anyway I fail to understand how OpenSSL generates this base64 output.

  • P-256 private key raw value is 32 bytes, although it is DER-encoded (correction!) as integer and (lazily) displayed by openssl as 33 bytes if it happens to have its highest bit set which will occur almost exactly half the time. – dave_thompson_085 Mar 23 '18 at 4:32

So you are right about the lengths. As Steffen Ullrich pointed out, you have a way to display the structure of the asn1 object by using openssl asn1parse.

If you want to extract the public key, and only the public key, you have to extract it from the asn1 object.

To do so:

openssl ec -in priv_key.pem -pubout -outform DER 2> /dev/null | \
dd bs=1 skip=$((2+2+19+2+1)) 2> /dev/null > raw_pub.key

You can verify that the key is 65 bytes long. Once base64 it is 88 like you said.

The meaning of $((2+2+19+2)) are the bytes you have to skip in order to reach the BIT STRING object that yields the public key. We also had an extra 1 byte to skip because the public key has the following form:

  • 0x00 for padding
  • 0x02 or 0x04 compressed/uncompressed
  • the actual public key payload

hl stands for header length I believe and it where the type of object is stored, so that explains all the 2s.

l stands for length and it is the length of the entire object.

d stands for depth and it indicates nested objects -- like you would have in json for example.

So the BIT STRING is at depth=1 after 2:d=1 hl=2 l= 19 cons: SEQUENCE. We have to add 19 bytes to our skip to reach the BIT STRING header which we skip -- another 2 bytes then we are at our destination.


As per the comments I've added and extra 1 byte because of the padding.

Source: rfc5480#section-2.2

  • Should be 2+2+19+3. For BITSTRING (only, not other DER types, thus not decoded by the generic parser) the first 'content' octet is not actual content but instead a count of unused=padding bits -- here always zero because X9.62/SEC1 point format is integral octets. – dave_thompson_085 Mar 23 '18 at 2:59
  • Actually I think it is +4 because the key should be 64 bytes -- 256 bits long. If I remember right, there is the type of the key append to the BITSTRING. I've just tested it and there are 0x00 0x04 in front of it every time. I've just glanced at the rfc. So it is 0x00 for padding and 0x04 for compressed or uncompressed. I'll make the changes. – tehmoon Mar 23 '18 at 14:31
  • Yes, 04 is for uncompressed but that is part of the point encoding; see the references. Compressed would be 02 or 03 plus 32 bytes X, versus uncompressed 04 plus 64 bytes X and Y. But the 00 for bitstring padding is ASN.1 overhead not part of the point. – dave_thompson_085 Mar 24 '18 at 1:55
  • I see you point, so yes I think you are right. – tehmoon Mar 24 '18 at 11:27

You don't get the raw bits of only the key when exporting it using openssl ec -pubout but a structure with more information:

$ openssl asn1parse -in pub_key.pem -dump
    0:d=0  hl=2 l=  89 cons: SEQUENCE          
    2:d=1  hl=2 l=  19 cons: SEQUENCE          
    4:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
   13:d=2  hl=2 l=   8 prim: OBJECT            :prime256v1
   23:d=1  hl=2 l=  66 prim: BIT STRING        
      0000 - 00 04 11 5e 44 51 b0 95-4e 9b 14 42 e7 60 21 10   ...^DQ..N..B.`!.
      0010 - f0 05 5d b4 bb 76 22 6d-2e 45 66 ad 3a 89 eb f2   ..]..v"m.Ef.:...
      0020 - 15 00 57 89 29 98 ce 84-5e b2 58 83 fb 8d 7a 59   ..W.)...^.X...zY
      0030 - 6d 11 2e 5d c3 7c e1 df-4a c9 ce a6 94 89 61 2a   m..].|..J.....a*
      0040 - dc 06

Thus, you get essentially 91 binary octets which translates to 124 characters when encoded as base64.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.