When PKI Becomes a Black Box
In everyday use, PKI often gets boiled down to “keys and certificates.” Most professionals know how to generate a key pair, install a certificate, or glance over a few standards. Tools make the process easy — and most documentation stops there, because going deeper is hard to explain.
But that surface knowledge only works until something breaks.
When a certificate suddenly stops working, it's like hitting a wall. Without understanding how a certificate is built — its structure, encoding, and validation rules — troubleshooting becomes guesswork. And that’s when the real headache begins.
Who Cares About ASN.1 and DER?
Ever wondered how PKI manages to organize sensitive data — like keys, signatures, and certificates — into a single file that can be saved, transmitted, and interpreted with bit-level precision?
That’s where ASN.1 and DER come in — two of the most overlooked yet critical foundations of PKI. Every time you generate a key, sign a certificate, or exchange cryptographic data, ASN.1 defines the structure, and DER ensures it’s encoded in a precise, unambiguous way. They work silently in the background, making interoperability between systems possible.
Put simply: ASN.1 defines what goes in — the object’s type, order, and validation rules. DER defines how it’s packed — the exact byte layout that guarantees the object can be reconstructed with no ambiguity.
RFC 5280 Section 4.1
Below is an excerpt from Section 4.1 of RFC 5280, which defines the basic structure of an X.509 v3 certificate:
4.1. Basic Certificate Fields
The X.509 v3 certificate basic syntax is as follows. For signature
calculation, the data that is to be signed is encoded using the ASN.1
distinguished encoding rules (DER) [X.690]. ASN.1 DER encoding is a
tag, length, value encoding system for each element.
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
A certificate contains three main components:
-
tbsCertificate
— short for To Be Signed Certificate. This section holds all the meaningful fields (issuer, subject, validity, public key, extensions) and is the exact portion that gets hashed and signed. -
signatureAlgorithm
— specifies the cryptographic algorithm used to sign the certificate, such as SHA-256 with RSA. It also appears inside thetbsCertificate
, so mismatches here can cause verification failures. -
signatureValue
— the digital signature generated by the CA. It's an encrypted hash of thetbsCertificate
, verifiable using the CA’s public key.
Here’s the definition of TBSCertificate
, also from RFC 5280:
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present, version MUST be v3
}
This syntax, taken directly from the RFC, shows how PKI uses ASN.1 to precisely structure certificate data — and how every field in your certificate file has a defined place and rule.
Briefly, What is DER?
DER (Distinguished Encoding Rules) is a binary encoding format that faithfully preserves the hierarchical structure of ASN.1 by using a TLV (Tag-Length-Value) format for every field.
- Tag indicates the data type of the value and whether it's a container (like a folder) or a leaf (like a file).
- Length specifies how many bytes follow.
- Value holds the actual content — or in the case of containers, it holds nested TLV-encoded fields.
Together, the Tag and Length form a header that precedes every value. The size of this header is variable (typically 2 to 4 bytes) depending on the data being described. This flexible header system is what allows DER to precisely maintain ASN.1’s tree structure, making it easy to convert between binary and structured ASN.1 representation.
Because DER is a binary format, you can inspect it using a hex viewer. I’m using the xxd
command on Linux to view the web-server-der.crt
file, which I created in the previous post of this blog series. This file will appear frequently throughout this article.
As described earlier, an X.509 certificate consists of three main components: TBS (To Be Signed), Signature Algorithm, and Signature Value. In the hex output below, I’ve included the full stream so you can see how each component starts with its own DER header (Tag + Length).
$ xxd -p web-server-der.crt
308202ac[ *1* ]30820194a0030201020214460d9cb0fd270f6e34f2b5c360c8b0
6fd47723b9300d06092a864886f70d01010b05003021311f301d06035504
030c16504b49574e4820496e7465726d656469617465204341301e170d32
35303830323136353932355a170d3237313130353136353932355a301a31
18301606035504030c0f7777772e6578616d706c652e636f6d3059301306
072a8648ce3d020106082a8648ce3d03010703420004f24ca98d334111f1
92d9dea71c0c2427f8141ba5516d8f58429969b4397f5e017ed56cd0e5b5
af1e079eaefd742cfd9128b38b9f32591e24c7b8b402af7d64caa381ad30
81aa300b0603551d0f0404030205a030130603551d25040c300a06082b06
01050507030130270603551d110420301e820f7777772e6578616d706c65
2e636f6d820b6578616d706c652e636f6d301d0603551d0e041604148fad
262431aa214fecf56740cf90f7634e66350a303e0603551d2304373035a1
1da41b30193117301506035504030c0e504b49574e4820526f6f74204341
8214445c36cc2042eb221b919df5bc9498d00f0cf583[ *2* ]300d06092a864886
f70d01010b0500[ *3* ]03820101002b917255013ea0192fae02fa2b5f0be93862
e1b1320e32af601ec8c709e3b6ee016d4a2fa65a5f505053732c0e0d3e46
12ce32ea0b3b303a0b1c9dd3d6a9d2b67d856adc08db8a8eb4ec66806d79
9503cd52fb36f797b139be01d3fb5fb2803668104adbd3e4d2a4ef1f90ca
f8a05575979706ba7dd484a61fbfc180321636296a76cf1fd365ad1b187f
ab1a8d202a7030c0268a05554630f8a3831fd968b62ad472f428e38be216
13572fb3b5f9009a6e8d5579632750780f3e91bbc614f3566106fa3f3e2e
f6077953875fabbe8bbafdb48e58768c5698c5beb9479b705aafbca89271
e820032a739eafdb0c5fc9063569220cf3920b0d185503f33935b996
# 1. TBS Certificate (SEQUENCE)
# Offset: 4
# Length: 0x0194 = 404 bytes
# 2. Signature Algorithm (SEQUENCE)
# Offset: 412 (0x019c)
# Length: 13 bytes (0x0d)
# 3. Signature Value (BIT STRING)
# Offset: 427 (0x01ab)
# Length: 0x0101 = 257 bytes
Back to the ASN.1
You might be wondering: Why bring ASN.1 back into the picture? It already structured the certificate data, and DER encoded it — so isn’t its job done? Surprisingly, no — and this is where ASN.1 really shines.
DER is so precise that we can reverse it back to exactly the same ASN.1 hierarchy, field by field. That’s the power of the TLV structure — it’s fully self-describing.
Try the following commands to convert a certificate back into ASN.1 format:
# For PEM-formatted certificates
openssl asn1parse -in cert-name.crt -i -dump
# For DER-formatted certificates
openssl asn1parse -in cert-name.crt -inform DER -i -dump
For example, parsing a DER-formatted certificate:
$ openssl asn1parse -in web-server-der.crt -inform DER -i -dump
0:d=0 hl=4 l= 684 cons: SEQUENCE
4:d=1 hl=4 l= 404 cons: SEQUENCE
8:d=2 hl=2 l= 3 cons: cont [ 0 ]
10:d=3 hl=2 l= 1 prim: INTEGER :02
13:d=2 hl=2 l= 20 prim: INTEGER :460D9CB0FD270F6E34F2B5C360C8B06FD47723B9
35:d=2 hl=2 l= 13 cons: SEQUENCE
37:d=3 hl=2 l= 9 prim: OBJECT :sha256WithRSAEncryption
# output truncated for brevity
You just saw how we used OpenSSL’s most underrated utility — asn1parse
— to decode a DER-formatted certificate and reveal its hierarchical ASN.1 structure.
Here’s a brief explanation of the output:
-
offset
— shows the byte position in the file where each TLV block starts -
hl
— header length (Tag + Length) -
d
— depth in the nested structure -
l
— value length -
cons
/prim
— whether the value is a container (like a folder) or a primitive (like a file) -
-dump
— reveals the actual value for each field
This tree-like output is what makes ASN.1 + DER so powerful — not just for encoding, but also for inspecting and verifying.
Manually Verifying a Certificate Signature
Now we’re about to do something you almost never see in blog posts — not even in documentation or tutorials. We're going to extract the tbsCertificate
and signatureValue
fields from an actual certificate and manually verify its digital signature using only basic Linux tools.
No OpenSSL shortcuts. No verify
command. Just raw data, hashing, and decryption — the way cryptographic verification actually works under the hood.
In Part 1, we created an Intermediate CA.
In Part 2, that CA issued a certificate to a web server.
Now in this post, you should already have the following files ready:
-
inter.key
— the private RSA key of the Intermediate CA (we’ll extract the public key from it) -
web-server-der.crt
— the DER-encoded X.509 certificate that we’re going to verify
Step 1: Extract tbsCertificate
We’ll use asn1parse
to inspect the raw structure of the certificate and locate the TBS section. Since it is a top-level field in the certificate structure, we can limit the output to a single depth using grep d=1
.
$ openssl asn1parse -inform DER -in web-server-der.crt | grep d=1
4:d=1 hl=4 l= 404 cons: SEQUENCE
412:d=1 hl=2 l= 13 cons: SEQUENCE
427:d=1 hl=4 l= 257 prim: BIT STRING
The first line corresponds to the tbsCertificate
, which starts at offset 4:
-
hl=4
→ the header (Tag + Length) is 4 bytes long -
l=404
→ the value is 404 bytes
So, to extract the full tbsCertificate
, we need a total of 4 + 404 = 408
bytes — starting at byte offset 4
.
This isn’t just a guess — it’s backed by the RFC:
#### RFC 5280 §4.1 – Basic Certificate Fields
> For signature calculation, the data that is to be signed is encoded using the ASN.1 distinguished encoding rules (DER) [X.690].
Now that we know the tbsCertificate
starts at offset 4 and spans 408 bytes (including its DER header), we can extract it from the certificate using the dd
command and save it to a file named tbs-cert.bin
.
dd if=web-server-der.crt of=tbs-cert.bin bs=1 skip=4 count=408 status=none
If the extraction was successful, you should be able to view the result with asn1parse
:
openssl asn1parse -inform DER -in tbs-cert.bin
Step 2: Extract signatureValue
Repeat the same step — but this time, our target is the third line of the parsed certificate:
$ openssl asn1parse -inform DER -in web-server-der.crt | grep d=1
4:d=1 hl=4 l= 404 cons: SEQUENCE
412:d=1 hl=2 l= 13 cons: SEQUENCE
427:d=1 hl=4 l= 257 prim: BIT STRING
At first glance, you might assume we should extract from offset 427
and grab 4 + 257 = 261
bytes — just like we did for the tbsCertificate
. But that would be incorrect.
This time, we're interested in the raw signature only, without the DER header.
So, should we start at 427 + 4 = 431
? Still no — because BIT STRING
fields in DER encoding include an extra padding byte right after the header. In most X.509 certificates, that byte is 00
, and it's not part of the actual signature.
So we need to skip:
- the 4-byte header
- and the 1-byte padding
That brings us to offset 432
, which is where the actual signature starts. Here’s the command to extract the 256-byte signature and save it to a file:
dd if=web-server-der.crt of=signature.bin bs=1 skip=432 count=256 status=none
If successful, signature.bin
will contain the raw signature that was generated by the issuing CA — and we’ll use this in the next step to manually verify it.
Step 3: Decrypt the CA’s Signature
We have inter.key
, the private RSA key of the issuing CA. Of course, a CA never shares its private key — so we’ll symbolically extract the public key from it, assuming that’s the only part we’d have in a real-world scenario:
openssl rsa -in inter.key -pubout -out inter-pub.key
Next, we’ll decrypt the signature using the issuer’s public key:
openssl pkeyutl -verifyrecover -in signature.bin -pubin -inkey inter-pub.key -out decrypted-digestinfo.bin
This command produces a file named decrypted-digestinfo.bin
, which contains the ASN.1 DigestInfo structure. To inspect it, run:
$ openssl asn1parse -in decrypted-digestinfo.bin -inform DER
0:d=0 hl=2 l= 49 cons: SEQUENCE
2:d=1 hl=2 l= 13 cons: SEQUENCE
4:d=2 hl=2 l= 9 prim: OBJECT :sha256
15:d=2 hl=2 l= 0 prim: NULL
17:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:3F91773C7CAB65B00412E28E3CDCD89A5D393FE3A41E732087259D92FCC7F42C
At this point, you've successfully decrypted the CA's digital signature and revealed the hash value it was protecting. In the next step, we'll independently hash the tbsCertificate
and compare the two values.
Step 4: Calculate the Hash of the TBS Part
Now hash the contents of tbs-cert.bin
using the expected digest algorithm — in this case, SHA-256:
$ openssl dgst -sha256 tbs-cert.bin
SHA2-256(tbs-cert.bin)= 3f91773c7cab65b00412e28e3cdcd89a5d393fe3a41e732087259d92fcc7f42c
In my case, the computed hash
3f91773c7cab65b00412e28e3cdcd89a5d393fe3a41e732087259d92fcc7f42c
exactly matches the hash we extracted from the decrypted signature in the previous step.
That’s it — you’ve just completed a manual end-to-end X.509 signature verification using raw bytes, OpenSSL, and no black-box tooling.
This is how you go from “I trust certificates because everyone does” to “I’ve verified it myself, byte for byte.”
PEM: The Real Purpose
The PEM format exists for one simple reason: to make our lives easier.
Without PEM, sharing something like inter.key
or web-server-der.crt
in this blog would’ve been nearly impossible. That’s because DER is binary, and you can't just copy and paste binary content into a blog post. But PEM is Base64-encoded text — human-readable, portable, and perfect for pasting into emails, terminals, or blog posts like this one.
In fact, if you were too lazy to read my previous two blog posts or build your own CA setup (with intermediate and server certificates), PEM saves the day.
Just open a text editor and paste the following into a file named inter-pub.key
:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngxvqcxp5499fcUG28Pf
lx3nO89NqGzFAbVIbIJ5sLaUsRmWIw9ZseAwysl85NCoiD2UgnozdJHPEnusC3gX
/8Djb44fNMKmcrFwATn8iU2J9vVFMB+bnVzDaTe1etEvexpHWlH4BOz2oV5iiqKV
XPdDJ9cmrE1Z81/MlRema8sMwOiN2ce32R9rT0hcAwVxYcyyVzF9tm3HZSfrufo9
JQwizTGiLgwkIkfb9LJBQs8YMQnqBmSfH5cZDr7/BJ5r2ep0iAKoVIXfvclkqNGu
mZMNFEFGxsrWH5hkCIrP+aW3+7aQSIV9xjnXSNnfzMDN90OqVMB30YJKUpJiLkLc
VwIDAQAB
-----END PUBLIC KEY-----
Then, paste the following into a file named web-server-pem.crt
:
-----BEGIN CERTIFICATE-----
MIICrDCCAZSgAwIBAgIURg2csP0nD2408rXDYMiwb9R3I7kwDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWUEtJV05IIEludGVybWVkaWF0ZSBDQTAeFw0yNTA4MDIx
NjU5MjVaFw0yNzExMDUxNjU5MjVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPJMqY0zQRHxktnepxwMJCf4FBul
UW2PWEKZabQ5f14BftVs0OW1rx4Hnq79dCz9kSizi58yWR4kx7i0Aq99ZMqjga0w
gaowCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCcGA1UdEQQgMB6C
D3d3dy5leGFtcGxlLmNvbYILZXhhbXBsZS5jb20wHQYDVR0OBBYEFI+tJiQxqiFP
7PVnQM+Q92NOZjUKMD4GA1UdIwQ3MDWhHaQbMBkxFzAVBgNVBAMMDlBLSVdOSCBS
b290IENBghREXDbMIELrIhuRnfW8lJjQDwz1gzANBgkqhkiG9w0BAQsFAAOCAQEA
K5FyVQE+oBkvrgL6K18L6Thi4bEyDjKvYB7Ixwnjtu4BbUovplpfUFBTcywODT5G
Es4y6gs7MDoLHJ3T1qnStn2FatwI24qOtOxmgG15lQPNUvs295exOb4B0/tfsoA2
aBBK29Pk0qTvH5DK+KBVdZeXBrp91ISmH7/BgDIWNilqds8f02WtGxh/qxqNICpw
MMAmigVVRjD4o4Mf2Wi2KtRy9Cjji+IWE1cvs7X5AJpujVV5YydQeA8+kbvGFPNW
YQb6Pz4u9gd5U4dfq76Luv20jlh2jFaYxb65R5twWq+8qJJx6CADKnOer9sMX8kG
NWkiDPOSCw0YVQPzOTW5lg==
-----END CERTIFICATE-----
Now convert this PEM certificate into DER format — just like the one I’ve used throughout this blog:
openssl x509 -in web-server-pem.crt -outform DER -out web-server-der.crt
These are the exact same key and certificate used in this blog. If you follow along, every single step — down to the signature bytes — will match. The only difference: you can skip Step 3 (extracting the public key), because you already have the public key as inter-pub.key
.
That’s the beauty of structured cryptographic data and true reproducibility — same input, same result, every time.
Note: I only shared the public key of the Intermediate CA — the private key remains secure, as it always should be.
Top comments (0)