What's in a private key? Fri Apr 16 2021 What's inside a PEM file? Find out where schemas for internal key files, how to read them, and a hint of DER and ASN.1! -------------------------------------------------------------------------------- What's in a private key? ======================== Published Apr 16, 2021 - 11 min read 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 [L1] 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 [L2] EC key, while the second part wraps it in PKCS#8 [L1]. -----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. 308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b02010104204ab2201c 33ba10fd4e05e685dea24f2f8189ec868d61f2eb2805968b33792e6ea144034200044aefefeb0f96 5169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6ec3c511b63d833108820adf 9e1ea2f479c35788d15ef0bce312e8618de3 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 [L3]) will come in a later post. [I1: March 2020] /[cendyne: excited]------------------------------------------------------------\ | Psst, hey listen! | | If you want to follow along with a useful website, check out ASN.1 | | JavaScript decoder [L4]. | \------------------------------------------------------------------------------/ If we parse this into ASN.1 [L5], 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 "306b02010104204ab2201c33ba10fd4e05e685dea24f2f8189ec868d61f2eb2805968b33792e6ea 144034200044aefefeb0f965169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6 ec3c511b63d833108820adf9e1ea2f479c35788d15ef0bce312e8618de3"} ) } | 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. 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 [L5] schema for that, as defined in PKCS#8 [L1]. 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 [L6] /[cendyne: disapprove]---------------------------------------------------------\ | The ISO [L7] 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 [L8], Elliptic curve public key cryptography, defined by IETF RFC3279 [L9] and RFC5753 [L10]. 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 [L11]. 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 [L2] ECC keys with 256 bits such as the secp256r1 curve (as correlated in SEC 2 [L12] 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 [L13] Table 24, NIST Special Publication 800-57 Part 1 [L14], Draft NIST Special Publication 800-186 [L15], RFC5903 [L16]. According to NIST Recommendations (keylength.com) [L17], this curve security strength may be used until 2030 and beyond. /[cendyne: 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 [L2] key! And according to the PKCS#8 [L1] 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 "044aefefeb0f965169fa9805f102ac61c3a988c15d3e9a5ad37d203cd9e67b9ef9e48f6ec3c511b 63d833108820adf9e1ea2f479c35788d15ef0bce312e8618de3" }) }) } The ASN.1 [L5] schema, according to the SEC 1 [L2] 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 [L2], 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. /--------------------------------------------------------------[cendyne: shrug]\ | Ed25519 actually uses little-endian instead. The private key encoding is | | completely dependent on the algorithm and parameter. More about Ed25519 in | | RFC8032 [L18]. | \------------------------------------------------------------------------------/ /--------------------------------------------------------------[cendyne: angel]\ | Hey if you're interested in Ed25519 keys and what's inside, check out A Deep | | dive into Ed25519 Signatures [L19]. | \------------------------------------------------------------------------------/ For the public key, this is actually described in SEC 1 [L2] 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. /[cendyne: 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. /[cendyne: angel]--------------------------------------------------------------\ | If you have any comments, feedback, or corrections, please let me know | | through my contact methods below! | \------------------------------------------------------------------------------/ -------------------------------------------------------------------------------- [L1]: https://tools.ietf.org/html/rfc5208 [L2]: https://www.secg.org/sec1-v2.pdf [L3]: https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf [L4]: https://lapo.it/asn1js/ [L5]: https://en.wikipedia.org/wiki/ASN.1 [L6]: https://www.itu.int/rec/T-REC-X.509-198811-S [L7]: https://www.iso.org/ [L8]: https://oidref.com/1.2.840.10045.2.1 [L9]: https://tools.ietf.org/html/rfc3279 [L10]: https://tools.ietf.org/html/rfc5753 [L11]: https://oidref.com/1.2.840.10045.3.1.7 [L12]: https://www.secg.org/sec2-v2.pdf [L13]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800- 56Ar3.pdf [L14]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800- 57pt1r5.pdf [L15]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186- draft.pdf [L16]: https://tools.ietf.org/html/rfc5903 [L17]: https://www.keylength.com/en/4/ [L18]: https://tools.ietf.org/html/rfc8032 [L19]: /posts/2022-03-06-ed25519-signatures.html [I1]: https://c.cdyn.dev/yERiydes