The Theory And Implementation Of An X.509 Certificate Authority: Asn.1, Signatures, And Ocsp

A comprehensive technical exploration of the theory and implementation of an x.509 certificate authority: asn.1, signatures, and ocsp, covering key concepts, practical implementations, and real-world applications.
Contents
The Theory And Implementation Of An X.509 Certificate Authority: Asn.1, Signatures, And Ocsp
Introduction
You’re browsing the web, and your browser flashes a warning: “Your connection is not private.” You click “Advanced” and see a cryptic message about a “missing intermediate certificate” or a “self-signed certificate in the certificate chain.” Most users simply turn back. But for a systems architect, a security engineer, or a curious developer, this is not a roadblock—it is a door.
That door opens into the silent, invisible infrastructure that holds much of the modern internet together: the Public Key Infrastructure (PKI). At the heart of every TLS handshake, every code-signed binary, and every encrypted email message sits a small, seemingly mundane data structure called an X.509 certificate. And behind that certificate stands an entity far more interesting than the certificate itself: the Certificate Authority (CA).
It is a strange paradox of our digital age that the entire edifice of secure communication—online banking, e-commerce, private messaging, and even the integrity of software updates—rests upon a system that most engineers consider “magic.” We throw around terms like chain of trust, root stores, and revocation checking as if they were self-evident truths. But between the act of requesting a certificate and the moment your browser displays that reassuring padlock icon, an astonishing amount of theory converges: abstract algebra, international standards for data representation, precise encoding rules, and real-time network protocols designed to answer a single, critical question: Is this certificate still trustworthy?
If you have ever deployed a private Kubernetes cluster, built an internal API gateway, or simply wanted to understand why Let’s Encrypt can issue certificates for free while other CAs charge hundreds of dollars, this blog post is for you. We are going to tear the X.509 certificate apart, examine its innards, and then put it back together—this time with a deeper understanding of how a certificate authority actually works. We’ll dive into ASN.1 (Abstract Syntax Notation One) the language used to define certificate structures, explore the mathematics of digital signatures and certificate chains, and dissect the Online Certificate Status Protocol (OCSP) that browsers use to check if a certificate has been revoked. Along the way, we’ll build a minimal but functional internal CA from scratch using OpenSSL and Python, and we’ll discuss real-world pitfalls that even experienced engineers overlook.
By the end of this article, you will not only be able to read a certificate’s raw bytes with confidence, but you will also understand the design decisions—both brilliant and problematic—that shape the PKI we all depend on.
1. The Anatomy of an X.509 Certificate
Before we can build a CA, we need to understand what it produces. An X.509 certificate, as defined by the ITU-T recommendation, is a structured collection of fields that bind a public key to an identity (a subject) through the digital signature of a trusted issuer (the CA). Let’s open up a real TLS certificate and walk through its components.
1.1 A Real Certificate Dissection
Use OpenSSL to fetch and display a certificate from a popular website:
openssl s_client -connect github.com:443 -showcerts </dev/null 2>/dev/null | \
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' | \
openssl x509 -text -noout
The output will contain many lines. Let’s highlight the most important fields:
- Version: Usually v3 (value 2).
- Serial Number: A unique integer assigned by the CA.
- Signature Algorithm: e.g.,
sha256WithRSAEncryption. - Issuer: The CA that signed this certificate (e.g., “DigiCert TLS RSA SHA256 2020 CA1”).
- Validity:
notBeforeandnotAfterdates. - Subject: The entity the certificate belongs to (e.g., “*.github.com”).
- Subject Public Key Info: The public key algorithm and its value (e.g., RSA 2048 bits or EC P-256).
- Extensions: A rich set of optional fields that carry additional constraints and usage flags. These include:
Subject Key Identifier(SKI)Authority Key Identifier(AKI)Key Usage(e.g., digitalSignature, keyEncipherment)Extended Key Usage(e.g., serverAuth, clientAuth)Basic Constraints(CA flag, path length constraint)Subject Alternative Names(SANs) – critical for modern browsers to match multiple hostnames.
Each of these fields is encoded using ASN.1, but more on that later.
1.2 The Role of the Serial Number
The serial number is more than a random number. It must be unique per certificate issued by a given CA. In practice, CAs generate serial numbers using cryptographic random numbers (at least 20 bytes) to prevent prediction and collisions. For our internal CA, we can use a simple counter or a UUID, but real CAs must ensure uniqueness even across restarts.
1.3 Validity Periods and the Clock Problem
The notBefore and notAfter dates define the certificate’s lifetime. Traditionally, certificates had long lifetimes (3–5 years). Today, industry best practices (e.g., Apple’s and Google’s mandates) limit TLS certificates to 398 days or less. This reduces the window of vulnerability if a private key is compromised, and it forces automation (like ACME) to become the norm. For an internal CA, we must decide on a reasonable expiry (e.g., 1 year) and plan for renewal or revocation.
1.4 Extensions: The Power and Complexity
Extensions are the reason we have X.509 v3. They allow PKI to be flexible. For example:
- Basic Constraints: The
CAflag indicates whether the subject can issue other certificates. This is how we build a chain: the root CA hasCA:TRUE, intermediates haveCA:TRUEwith a path length, and leaf certificates haveCA:FALSE. - Key Usage: A leaf server certificate might have
digitalSignatureandkeyEncipherment(for RSA key exchange) but notcertSign. - Subject Alternative Names: Without SANs, a certificate could only protect a single hostname (via the Common Name in Subject). Modern browsers ignore CN and require SANs for multiple domains.
Understanding these fields is essential because misconfiguration can break security guarantees. For example, if a leaf certificate has CA:TRUE set inadvertently, any holder of that certificate could issue fraudulent certs.
2. ASN.1: The Language of Certificates
X.509 certificates are not plain JSON or XML. They are encoded using ASN.1 (Abstract Syntax Notation One) and then serialized using one of several encoding rules. The most common for certificates is DER (Distinguished Encoding Rules), a subset of BER (Basic Encoding Rules) that guarantees unique byte representation. PEM (Privacy-Enhanced Mail) is just Base64-encoded DER with header/footer lines.
2.1 Understanding ASN.1 Syntax
ASN.1 is a schema definition language with a concise notation. For example, the X.509 certificate structure (simplified) looks like this:
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING
}
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,
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
extensions [3] EXPLICIT Extensions OPTIONAL
}
Each field is tagged with a type (SEQUENCE, INTEGER, BIT STRING, etc.) and sometimes with an explicit context tag (like [0]). This schema is what parsers use to decode the byte stream.
2.2 TLV Encoding: Tag, Length, Value
DER encoding is a form of Type-Length-Value (TLV). Every element starts with a tag byte (or bytes) that identifies the type. Then the length of the value, then the value itself. For example, an INTEGER with value 42 would be encoded as:
- Tag:
0x02(universal tag for INTEGER) - Length:
0x01(one byte) - Value:
0x2A
For constructed types like SEQUENCE, the tag is 0x30, followed by the length of the entire contents.
Let’s write a small Python script to parse a DER-encoded certificate manually:
import socket
import ssl
# Get DER bytes from a website
hostname = "github.com"
port = 443
ctx = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
cert_der = ssock.getpeercert(binary_form=True)
# Now manually parse the first SEQUENCE
def parse_tlv(data, offset):
tag = data[offset]
length_bytes = 1
length = data[offset+1]
if length & 0x80:
num_len_bytes = length & 0x7f
length = int.from_bytes(data[offset+2:offset+2+num_len_bytes], 'big')
length_bytes = 1 + 1 + num_len_bytes
else:
length_bytes = 2
value_start = offset + length_bytes
value = data[value_start:value_start+length]
return tag, length, value, value_start+length
tag, length, value, next_offset = parse_tlv(cert_der, 0)
print(f"Tag: 0x{tag:02x}, Length: {length}")
# Expected tag 0x30 (SEQUENCE)
This simplistic parser won’t handle all cases (e.g., indefinite length), but it illustrates the TLV structure.
2.3 Encoding Practicalities and Pitfalls
Because DER is binary and unforgiving, many tools output PEM for readability. When implementing a CA, you must handle both formats. OpenSSL’s -inform DER and -inform PEM flags are your friends.
A common encoding mistake: using BER (Basic Encoding Rules) instead of DER. BER allows multiple ways to represent the same data (e.g., indefinite length), which breaks signatures because the same data must always produce the same bytes. That’s why certificates require DER: the signed portion (TBS Certificate) must be byte-identical for the signature to verify.
2.4 ASN.1 Tools and Debugging
To inspect a certificate’s ASN.1 structure, you can use:
openssl asn1parse -in cert.pem -inform PEM
This shows the full tree with offsets, tag numbers, and lengths. It’s invaluable when debugging why a certificate is malformed.
For programmatic ASN.1 manipulation in Python, the pyasn1 library is excellent. Here’s an example of encoding a simple TBSCertificate:
from pyasn1.codec.der import encoder
from pyasn1.type import univ, tag
# Define a simple integer
my_int = univ.Integer(12345)
encoded = encoder.encode(my_int)
print(encoded.hex())
But for real certificate creation, we’ll rely on OpenSSL’s library or high-level wrappers like cryptography.
3. Digital Signatures and Certificate Chains
A certificate is only as trustworthy as the signature on it. Digital signatures are the glue that creates a chain of trust from a root CA to a leaf server certificate.
3.1 How a Signature is Created and Verified
Given a TBSCertificate (the data to be signed), the CA:
- Hashes the DER-encoded TBSCertificate using a chosen hash algorithm (e.g., SHA-256).
- Encrypts (or signs) that hash with its private key using the given signature algorithm (e.g., RSA with PKCS#1 v1.5 padding, or ECDSA).
- Places the resulting bit string into the
signatureValuefield.
To verify, a verifier:
- Reads the
signatureAlgorithmfield to know the algorithm. - Decodes the TBSCertificate and computes its hash.
- Uses the issuer’s public key (found in the parent certificate) to decrypt the signature and compare the recovered hash.
If they match, the certificate is authentic and unmodified.
3.2 The Chain of Trust
When a browser connects to a TLS server, the server sends its certificate plus optionally intermediate certificates. The browser must build a path from the server’s leaf certificate up to a trusted root certificate stored in its root store.
Example chain:
Leaf (github.com)
signed by intermediate CA1 (DigiCert TLS RSA SHA256 2020 CA1)
signed by root CA (DigiCert Global Root CA)
The root certificate is self-signed (its issuer and subject are the same, and the signature is verified using its own public key). Root certificates are trusted by virtue of being pre-installed in the browser/OS trust store. Intermediates are signed by the root (or by another intermediate). The leaf is signed by an intermediate.
This chain structure allows the root CA to keep its private key offline (in a Hardware Security Module, HSM), while intermediates are used for day-to-day issuance. If an intermediate is compromised, it can be revoked without re-installing roots globally.
3.3 Building the Chain in Code
When programming a CA, you need to be able to verify a chain. In Python, using the cryptography library:
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# Load leaf certificate
with open("leaf.pem", "rb") as f:
leaf = x509.load_pem_x509_certificate(f.read(), default_backend())
# Load intermediate
with open("intermediate.pem", "rb") as f:
intermediate = x509.load_pem_x509_certificate(f.read(), default_backend())
# Load root
with open("root.pem", "rb") as f:
root = x509.load_pem_x509_certificate(f.read(), default_backend())
# Verify that leaf was signed by intermediate
intermediate_pub_key = intermediate.public_key()
try:
intermediate_pub_key.verify(
leaf.signature,
leaf.tbs_certificate_bytes,
# Need to determine padding/algorithm from leaf.signature_algorithm_oid
# Simplified: use default for RSA
...
)
except InvalidSignature:
print("Leaf not valid under intermediate")
A robust CA implementation must handle various signature algorithms (RSA with different paddings, ECDSA, Ed25519) and OID mapping.
3.4 Signature Algorithms: ECDSA vs RSA vs EdDSA
- RSA (Rivest–Shamir–Adleman): Still dominant because of compatibility, but key sizes are larger (2048, 4096 bits). Signature generation is slower than EC, but verification is fast. PKCS#1 v1.5 padding is common, but PSS (Probabilistic Signature Scheme) is more modern and recommended for new systems.
- ECDSA (Elliptic Curve Digital Signature Algorithm): Smaller keys (256 bits provides equivalent security to 3072-bit RSA), faster signing, but verification can be slower than RSA. Widely used in modern ecosystems (e.g., Let’s Encrypt uses ECDSA P-384 for intermediates).
- EdDSA (Ed25519, Ed448): The newest, fastest, and most secure option. Not yet universally supported by all TLS libraries, but gaining traction.
When implementing a CA, the choice affects performance, security, and interoperability. For an internal CA, Ed25519 is a good choice if all clients support it.
4. Implementing a Minimal Certificate Authority
Now we move from theory to practice. We’ll build a simple internal CA that can issue leaf certificates for a private domain. We’ll use OpenSSL for the heavy lifting but also write a Python script for automation.
4.1 The Root CA
First, generate a root CA private key and self-signed root certificate.
# Generate ECDSA private key using P-384
openssl ecparam -genkey -name secp384r1 -out rootCA.key
# Self-sign root certificate (valid for 10 years)
openssl req -x509 -new -nodes -key rootCA.key -sha384 -days 3650 \
-out rootCA.crt \
-subj "/C=US/ST=California/L=San Francisco/O=MyOrg/CN=MyRootCA"
Important flags:
-nodesmeans no DES encryption on private key (for automated environments, you should encrypt with a passphrase or use HSM).-sha384is a reasonable hash algorithm; avoid SHA-1.- Days limited to 10 years; shorter is better practice.
Now verify:
openssl x509 -in rootCA.crt -text -nooutput | grep -A2 "Basic Constraints"
Should show CA:TRUE.
4.2 The Intermediate CA
A root CA should not sign leaf certificates directly. Create an intermediate CA.
# Generate intermediate private key
openssl ecparam -genkey -name secp384r1 -out intermediate.key
# Create a certificate signing request (CSR)
openssl req -new -key intermediate.key -out intermediate.csr \
-subj "/C=US/ST=California/L=San Francisco/O=MyOrg/CN=MyIntermediateCA"
# Sign the intermediate CSR with the root CA
openssl x509 -req -in intermediate.csr \
-CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out intermediate.crt -days 1825 -sha384 \
-extfile <(echo "basicConstraints=CA:TRUE,pathlen:0")
The pathlen:0 means this intermediate can issue leaf certificates but not further intermediates. This is a safety measure.
4.3 Issuing a Leaf Certificate
Now use the intermediate to issue a certificate for, say, app.internal.example.com.
# Generate leaf private key
openssl ecparam -genkey -name secp384r1 -out leaf.key
# Create CSR
openssl req -new -key leaf.key -out leaf.csr \
-subj "/C=US/ST=California/L=San Francisco/O=MyOrg/CN=app.internal.example.com"
# Sign with intermediate, adding appropriate extensions
openssl x509 -req -in leaf.csr \
-CA intermediate.crt -CAkey intermediate.key -CAcreateserial \
-out leaf.crt -days 398 -sha384 \
-extfile <(printf "basicConstraints=CA:FALSE\nsubjectAltName=DNS:app.internal.example.com,DNS:internal.example.com")
The subjectAltName line adds SANs. Without them, modern browsers (Chrome, Firefox) will reject the certificate for TLS.
Now test chain:
# Concatenate leaf + intermediate + root for verification
cat leaf.crt intermediate.crt rootCA.crt > chain.pem
# Verify
openssl verify -CAfile rootCA.crt -untrusted intermediate.crt leaf.crt
If you get leaf.crt: OK, the chain is valid.
4.4 Automating with Python (Optional)
For a real internal CA, you’d want to script certificate issuance, renewal, and revocation. Here’s a minimal Python snippet using cryptography:
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
import datetime
# Load intermediate CA private key and cert
with open("intermediate.key", "rb") as f:
ca_private_key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
with open("intermediate.crt", "rb") as f:
ca_cert = x509.load_pem_x509_certificate(f.read(), default_backend())
# Generate leaf key
leaf_private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())
# Build subject
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MyOrg"),
x509.NameAttribute(NameOID.COMMON_NAME, "app.internal.example.com"),
])
# Build cert
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ca_cert.subject)
builder = builder.public_key(leaf_private_key.public_key())
builder = builder.serial_number(x509.random_serial_number())
builder = builder.not_valid_before(datetime.datetime.utcnow())
builder = builder.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=398))
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None), critical=True)
builder = builder.add_extension(
x509.SubjectAlternativeName([x509.DNSName("app.internal.example.com")]),
critical=False)
# Sign with CA
leaf_cert = builder.sign(ca_private_key, hashes.SHA384(), default_backend())
# Save
with open("leaf.crt", "wb") as f:
f.write(leaf_cert.public_bytes(serialization.Encoding.PEM))
This script gives you full control over extensions and OIDs.
5. Certificate Revocation and OCSP
A certificate can become invalid before its expiry date—for instance, if the private key is leaked, or the domain is decommissioned. The CA must provide a way to check revocation status. There are two main mechanisms: Certificate Revocation Lists (CRLs) and the Online Certificate Status Protocol (OCSP). We’ll focus on OCSP because it’s real-time and more scalable.
5.1 How OCSP Works
OCSP is defined in RFC 6960. The client (e.g., a browser) sends an HTTP request containing the certificate’s issuer name and serial number (among other things) to an OCSP responder URL, which is typically listed in the certificate’s Authority Information Access extension. The responder replies with a signed response indicating good, revoked, or unknown.
The OCSP response is itself a DER-encoded structure signed by the CA (or by a delegated OCSP responder). The client must verify the signature using the CA’s public key (or the delegated responder’s certificate).
5.2 Setting Up an OCSP Responder
For our internal CA, we can run a simple OCSP responder using OpenSSL’s openssl ocsp command.
First, we need to generate an OCSP responder certificate (or use the intermediate CA itself). Steps:
- Create a dedicated OCSP signing certificate signed by the intermediate CA, with extended key usage
OCSPSigning(OID 1.3.6.1.5.5.7.3.9). - Create an index file and serial number file that track issued certificates and their status.
Create a file index.txt (empty) and a file serial with a starting number such as 1000.
Now start the responder (example only, not for production):
openssl ocsp -port 8080 -text \
-index index.txt -CA intermediate.crt \
-rkey ocsp_signing.key -rsigner ocsp_signing.crt \
-nrequest 1000
Clients would request http://ocsp.internal.example.com:8080/OCSP. But in a real internal environment, you’d likely run a proper server like ocspd or a Python-based responder.
5.3 OCSP Stapling
To reduce latency and privacy concerns (the OCSP responder learns which sites a user visits), TLS servers can “staple” an OCSP response to the TLS handshake. The server periodically fetches the OCSP response from the CA and sends it to clients during the handshake. This requires the server to support OCSP stapling (e.g., Nginx with ssl_stapling on).
5.4 Revocation Procedure
To revoke a certificate, you must update the CA’s database (index file) and generate a new CRL or update OCSP response. In OpenSSL, you use:
openssl ca -revoke leaf.crt -keyfile intermediate.key -cert intermediate.crt
This updates index.txt with a R (revoked) entry. Then generate a new CRL:
openssl ca -gencrl -keyfile intermediate.key -cert intermediate.crt -out intermediate.crl
Clients would download the CRL periodically. For OCSP, the responder reads the index file live.
5.5 Challenges with OCSP
- Privacy: The OCSP responder sees every client’s IP address and which certificate they check.
- Availability: If the OCSP responder is down, some clients may fail the connection (soft-fail vs hard-fail modes). Most browsers soft-fail, allowing connections if revocation status cannot be checked—which is a security trade-off.
- Performance: OCSP responders must handle many requests. CDN-based solutions exist.
For an internal CA, where networks are controlled and latency is low, running your own OCSP responder is feasible. However, many internal deployments skip revocation and rely on short-lived certificates (e.g., 24 hours) to avoid the need for revocation.
6. Real-World Examples and Pitfalls
6.1 The Lean Startup: No OCSP, Short Lifetimes
Many cloud-native deployments (e.g., Kubernetes with cert-manager and Let’s Encrypt) use short-lived certificates (90 days) and rely on automatic renewal. Revocation is rarely used because the window of compromise is small. This is a practical trade-off that simplifies the PKI.
6.2 The Entrenched Enterprise: Long-Lived Roots and HSM
Large enterprises often have long-lived root CAs (20 years) stored in hardware security modules (HSMs) that never touch a network. Intermediates are issued for different departments. Revocation is critical because certificates may live for years. They run OCSP responders and maintain CRLs.
6.3 Common Mistakes
- Overlapping validity periods: When roots expire before intermediates, the chain breaks. Always set root validity longer than all subordinates.
- Missing SANs: Developers often set the Common Name and forget SANs. Modern browsers ignore CN and fail.
- Incorrect key usage: A certificate with
keyCertSignbut withoutbasicConstraints=CA:TRUEis invalid as a CA. - Weak hash algorithms: Using SHA-1 for signatures is no longer acceptable; many browsers block them.
- Private key permissions: Storing private keys with world-readable permissions is a security nightmare. Use
chmod 400and consider HSMs. - Not renewing in time: Automated renewal with ACME is the gold standard; manual processes lead to outages.
6.4 Case Study: The Symantec / DigiNotar Disasters
In 2011, DigiNotar, a Dutch CA, suffered a breach that led to the issuance of fraudulent Google certificates. The result: all DigiNotar roots were distrusted, and the company went bankrupt. This highlights the importance of CA security practices: offline roots, rigorous audit controls, and timely revocation.
Symantec (later acquired by DigiCert) ran into issues with mis-issued certificates, leading major browser vendors to distrust their roots unless they were replaced by DigiCert. The lesson: CA operations must be transparent and audited.
7. Conclusion
We have traveled from the humble warning message “Your connection is not private” to the intricate world of ASN.1 encoding, digital signature mathematics, and real-time revocation protocols. The X.509 certificate authority is a masterwork of systems design—a perfect example of how abstract principles (public key cryptography, recursive trust, and formal data representation) become the invisible foundation for everyday secure communication.
What have we learned? That a certificate is not a simple blob of bytes but a carefully constructed data structure with extensions, constraints, and signatures that propagate trust. That ASN.1, despite its age and verbosity, provides a rigorous encoding that ensures signatures are deterministic. That revocation, while essential, is often deferred in practice in favor of short-lived certificates and automation.
For the engineer building an internal PKI, the key takeaways are:
- Plan your hierarchy: root offline, intermediates online, leaves automated.
- Use modern algorithms: ECDSA or Ed25519 over RSA where possible; SHA-256 or SHA-384.
- Automate everything: certificate issuance, renewal, and revocation. Use ACME tools or custom scripts.
- Secure your private keys: HSM for roots, encrypted files for intermediates, minimal permissions.
- Test your chain: verify with
openssl verifyand browser testing.
The door that once read “Your connection is not private” is now open. You have the knowledge to walk through it—not as a user, but as a builder.
Further reading: RFC 5280 (Internet X.509 PKI), RFC 6960 (OCSP), ITU-T Recommendation X.509, and the OpenSSL documentation.