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