Cendyne.dev Posts

What's in a private key? - 2021-04-16

When you got a PEM to sign or encrypt with, what's inside? Read below to get a grasp on what's inside a PEM file, where schemas for things are and how to read them, and a hint of DER and ASN.1!

To start, let's generate a PKCS#8 private key with a command like so.

openssl ecparam -name prime256v1 -genkey -noout | \
  openssl pkcs8 -topk8 -nocrypt -outform pem
Note:
The first generates a SEC 1 EC key, while the second part wraps it in PKCS#8.
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSrIgHDO6EP1OBeaF
3qJPL4GJ7IaNYfLrKAWWizN5Lm6hRANCAARK7+/rD5ZRafqYBfECrGHDqYjBXT6a
WtN9IDzZ5nue+eSPbsPFEbY9gzEIggrfnh6i9HnDV4jRXvC84xLoYY3j
-----END PRIVATE KEY-----

Let's take a moment and break down this PEM key.

Note:
This key is created for this post and is not used for any content outside of this post.

First, let's convert the base64 body to hex, it'll make searching for repeated sequences such as 044aefef easier.

308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b02010104204ab2201c33ba10fd4e05e685dea24f2f8189ec868d61f2eb2805968b33792e6ea144034200044aefefeb0f965169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6ec3c511b63d833108820adf9e1ea2f479c35788d15ef0bce312e8618de3

Below is another representation of the same hex string, ASN.1 has tags (indicated in blue), the byte size of the tag (indicated in teal), and the contents of each tag (left in black). A more thorough introduction to the binary structure of Distinguished Encoding Rules (or DER) will come in a later post.

March 2020
excited
Psst, hey listen!
If you want to follow along with a useful website, check out ASN.1 JavaScript decoder.

If we parse this into ASN.1, we get the structure as follows:

{:type :sequence :value (
  {:type :integer :value 0}
  {:type :sequence :value (
    {:type :object-identifier :value "1.2.840.10045.2.1"}
    {:type :object-identifier :value "1.2.840.10045.3.1.7"}
    )
  }
  {:type :octet-string :value "306b02010104204ab2201c33ba10fd4e05e685dea24f2f8189ec868d61f2eb2805968b33792e6ea144034200044aefefeb0f965169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6ec3c511b63d833108820adf9e1ea2f479c35788d15ef0bce312e8618de3"}
  )
}
First time seeing code like the above? It's lisp like dictionary / map constructed by a list of key value, key value. Read the words rather than focus on the syntax.
surprise

There's an opaque blob in here too. So if we rip it out then

308187020100301306072a8648ce3d020106082a8648ce3d030107046d

is responsible for

{:type :sequence :value (
  {:type :integer :value 0}
  {:type :sequence :value (
    {:type :object-identifier :value "1.2.840.10045.2.1"}
    {:type :object-identifier :value "1.2.840.10045.3.1.7"}
    )
  }
  :omitted-content
  )
}

Up here you'll see a couple object identifiers, a number, and then the opaque blob. Here's the ASN.1 schema for that, as defined in PKCS#8. These schemas are a bit old and crusty, but they do the job.

PrivateKeyInfo ::= SEQUENCE {
  version                   Version,
  privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
  privateKey                PrivateKey,
  attributes           [0]  IMPLICIT Attributes OPTIONAL
}
PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
PrivateKey ::= OCTET STRING
Attributes ::= SET OF Attribute

And AlgorithmIdentifier comes from X.509

disapprove
The ISO has made this standard (or the latest version) unavailable to the public without payment. I consider the ISO to be a detriment to society with their regressive participation in standards forming and adoption.

AlgorithmIdentifier ::= SEQUENCE {
  algorithm       OBJECT IDENTIFIER,
  parameters      ANY DEFINED BY algorithm OPTIONAL
}

So the opaque section identified above is the private key as an octet string (essentially just a byte array). While the version is fixed at 0, and the algorithm seems to be... Well whatever these object identifiers are.

Inspecting the first: OID 1.2.840.10045.2.1, Elliptic curve public key cryptography, defined by IETF RFC3279 and RFC5753. So that is the private key algorithm (or class of algorithms) While it's parameter is.. well that comes next: OID 1.2.840.10045.3.1.7. Its description is 256 bit elliptic curve (szOID_ECC_CURVE_P256) So we can see now that we are using an Elliptic curve cryptography private key with the secp256r1 curve.

Note:
An elliptic key curve is a complex topic for another post, but in short it's a standardized set of values that can reproduce a mathematical calculation which is hard to reverse.
Note:
According to table 3 in SEC 1 ECC keys with 256 bits such as the secp256r1 curve (as correlated in SEC 2 table 1) has a security strength of 128, a size of 256, and an effective RSA comparison of 3072 bits. Other references include.. NIST Special Publication 800-56A Table 24, NIST Special Publication 800-57 Part 1, Draft NIST Special Publication 800-186, RFC5903. According to NIST Recommendations (keylength.com), this curve security strength may be used until 2030 and beyond.
shrug
The r in secp256r1 happens to stand for random, while in k refers to Koblitz, secp256k1 is often used in cryptocurrency due to other optimizations available for this curve group parameter. When NIST published the curve parameters for secp256r1 they labeled it as random. Whether or not NIST provided random or a set of numbers that provide them an advantage has been an unresolved debate.

So what's in the opaque section? Actually it's a SEC 1 key! And according to the PKCS#8 private key info above, a secp256r1 key.

{:type :sequence :value (
  {:type :integer :value 1}
  {:encoding :hex :type :octet-string
    :value "4ab2201c33ba10fd4e05e685dea24f2f8189ec868d61f2eb2805968b33792e6e"
  }
  {:constructed true :tag 1 :type :context-specific
    :value ({:bits 520 :encoding :hex :type :bit-string
      :value "044aefefeb0f965169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6ec3c511b63d833108820adf9e1ea2f479c35788d15ef0bce312e8618de3"
    })
  })
}

The ASN.1 schema, according to the SEC 1 for an EC Private key is

ECPrivateKey ::= SEQUENCE {
  version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
  privateKey OCTET STRING,
  parameters [0] ECDomainParameters {{ SECGCurveNames }} OPTIONAL,
  publicKey [1] BIT STRING OPTIONAL
}

So you can see the private key has a version (a constant set to 1) and an OCTET STRING for the private key value (an arbitrary in value with the appropriate bit length). The private key's length and contents are specific to the key type, which was designated via the object identifiers in the PKCS8 key wrap above. Following that are any domain parameters, in this case none were included as the key type specifies the parameters for the curve. Lastly the public key, which again is specific to the type of key in particular. For whatever reason, it is a common trope for the public key to be a bit string, rather than an octet string.

Note:
The first byte of a bit string is usually how many bits should be shifted for the big-endian integer that follows. When it is divisible by 8, meaning the whole number is represented with full byte values, then you'll find the hex 0x00 prior to the contents, the contents starting with 0x04.

According to SEC 1, the private key content is a big-endian big integer for this particular type of ECC key. There's no further meaning in this integer, besides keeping it secret.

Ed25519 actually uses little-endian instead. The private key encoding is completely dependent on the algorithm and parameter. More about Ed25519 in RFC8031.
shrug

For the public key, this is actually described in SEC 1 Section 2.3.3 In this example, point compression is not used, as indicated by the leading byte 0x04. What follows is the X coordinate fit to 32 bytes (as the key is 256 bits, per the curve configuration), and the Y coordinate likewise.

Note:
Point compression allows an already short public key, at least in comparison to RSA, to be even shorter, such that only a single byte is used to signify if it is an "odd" or "even" prime. Some call the bit for this the sign, but these numbers don't occur below zero. If the public key starts with 0x02 or 0x03, then point compression is used. Again, the above example has 0x04 for an uncompressed point.
panic
Point compression has lead to several vulnerable implementations which do not validate input points for the sake of speed. To uncompress a point, a non trivial calculation must be made to derive two points, which are then selected based on the sign bit in the leading byte. If a calculation is performed without validating, the validation asserting the point is on the curve, then the private key of the user may be leaked. As of late, misuse resistant designs have been prioritized and "Don't roll your own crypto" repeated over and over.

If you'd like to inspect this key file, what you can do is:

$ openssl ec -text -in key.pem -noout
read EC key
Private-Key: (256 bit)
priv:
    4a:b2:20:1c:33:ba:10:fd:4e:05:e6:85:de:a2:4f:
    2f:81:89:ec:86:8d:61:f2:eb:28:05:96:8b:33:79:
    2e:6e
pub:
    04:4a:ef:ef:eb:0f:96:51:69:fa:98:05:f1:02:ac:
    61:c3:a9:88:c1:5d:3e:9a:5a:d3:7d:20:3c:d9:e6:
    7b:9e:f9:e4:8f:6e:c3:c5:11:b6:3d:83:31:08:82:
    0a:df:9e:1e:a2:f4:79:c3:57:88:d1:5e:f0:bc:e3:
    12:e8:61:8d:e3
ASN1 OID: prime256v1
NIST CURVE: P-256

Note:
Printing key content is specific to the key type, so for rsa use openssl rsa instead of openssl ec

That was quite a wandering way to peer into a key file, but it demonstrates how to approach dissecting PEM content going forward.

So, what's the private key?

d: 33785871188901339266591346360023059825676061520759082182150047705565125750382

And the public key?

x: 33895083098249617942691657925075450393182366388506933082364507268911843745529
y: 103380753077289600266558146853629830711510703014701325542323242537280776277475

Not too riveting I know. But what you can do with this can be!

That wraps it up for this post, I hope you've learned more about what's in a key .pem file and a basic understanding of navigating standards documents and schemas.

angel
If you have any comments, feedback, or corrections, please let me know through my contact methods below!