Is this certificate DER or PEM encoded ? It turns out, both at the same time

On March 15, 2025 by Sosthène Guédon

X509 certificate can be encoded either as DER or PEM. DER encoding is an efficient binary format, while PEM encoding is a wrapper around the Base 64 DER encoding of the certificate.

Usually, when dealing with a specific certificate, you know beforehand whether it's encoded as DER or PEM. For example, in the opennssl CLI, you can give it the -inform parameter, which accepts either DER or PEM.

However, what if don't know the encoding of the certificate, can you figure it out on the fly?

The PEM format is defined in RFC 7468. It has three parts: a preamble, in the form of -----BEGIN CERTIFICATE----- followed by the Base 64 encoding of the DER version of the certificate, then the trailer -----END CERTIFICATE-----.

This can make one think that it's possible to differentiate the two formats of the file by just checking whether the file starts with -----BEGIN CERTIFICATE-----. However, the spec says that this is not enough:

Data before the encapsulation boundaries are permitted, and parsers MUST NOT malfunction when processing such data.

This means that a PEM-encoded certificate can include data before the certificate itself. This means that if we can embed a PEM encoded certificate in one of the values inside a DER encoded certificate, we will have one file that can be parsed as two distinct certificates depending on whether it's interpreted as PEM or DER encoding.

For example, here is a python script that generates a certificate, and then interprets it as two different values, parsing it once assuming it's DER encoded:

from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import datetime

private_key = Ed25519PrivateKey.generate()

certificate_builder = x509.CertificateBuilder()
domain_component = ["""
-----BEGIN CERTIFICATE-----
MIIBgDCCATKgAwIBAgIUUZedX2PRkE2W95Tcf0KvyT89kVEwBQYDK2VwMF8xKjAR
BgoJkiaJk/IsZAEZFgNjb20wFQYKCZImiZPyLGQBGRYHZXhhbXBsZTExMAsGA1UE
AwwEZGVtbzAOBgNVBAMMB2V4YW1wbGUwEgYDVQQDDAtjZXJ0aWZpY2F0ZTAgFw0w
MDAxMDEwMDAwMDBaGA8yMDk5MDEwMTAwMDAwMFowXzEqMBEGCgmSJomT8ixkARkW
A2NvbTAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMTEwCwYDVQQDDARkZW1vMA4GA1UE
AwwHZXhhbXBsZTASBgNVBAMMC2NlcnRpZmljYXRlMCowBQYDK2VwAyEATX1Ud7XN
vdIv02Mf3gZSR+oFvhYEX3xWKamUUXThtK4wBQYDK2VwA0EA9QaR6uz/9i6dHKmP
v20IH22aLrczHybXTXpQ59zNKHEN69G2kBnd0ckV/WJ+bGCTvinulpOHY5SeJ64O
I7csAw==
-----END CERTIFICATE-----
""","example","com"]
domain_component_issuer = ["example","com"]
subject_name = ["demo", "example", "certificate"]
crypto_rdns = x509.Name(
    [
        x509.RelativeDistinguishedName(
            [
                x509.NameAttribute(x509.NameOID.DOMAIN_COMPONENT, subject)
                for subject in domain_component
            ]
        ),
        x509.RelativeDistinguishedName(
            [
                x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)
                for subject in subject_name
            ]
        ),
    ]
)
crypto_rdns_issuer = x509.Name(
    [
        x509.RelativeDistinguishedName(
            [
                x509.NameAttribute(x509.NameOID.DOMAIN_COMPONENT, subject)
                for subject in domain_component_issuer
            ]
        ),
        x509.RelativeDistinguishedName(
            [
                x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)
                for subject in subject_name
            ]
        ),
    ]
)

certificate_builder = (
    certificate_builder.subject_name(crypto_rdns)
    .issuer_name(crypto_rdns_issuer)
    .not_valid_before(datetime.datetime(2000, 1, 1, 0, 0))
    .not_valid_after(datetime.datetime(2099, 1, 1, 0, 0))
    .serial_number(x509.random_serial_number())
    .public_key(private_key.public_key())
)

certificate = certificate_builder.sign(private_key, None)

certificate_bytes = certificate.public_bytes(Encoding.DER)

with open("doublecert.der", "wb") as f:
    f.write(certificate_bytes)

The script is pretty simple, we generate a P256 keypair, then a self-signed certificate, except that in the Domain Component (DC) of the certificate, we include a full PEM encoded certificate. Then we add all the necessary values to build a certificate. We need to include newlines before the preamble and after the trailer, otherwise the openssl CLI tool will not accept the certificate.

The script then writes the certificate in the doublecert.der file.

We can then parse it:

  • as DER: openssl x509 -in doublecert.der -noout -text -inform DER
  • as PEM: openssl x509 -in doublecert.der -noout -text -inform PEM

This will give us two distinct certificates!

The DER one, which includes inside of it the PEM certificate

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            4b:10:47:12:ac:84:ab:42:59:12:67:a4:24:8e:23:d0:25:5e:81:70
        Signature Algorithm: ED25519
        Issuer: DC=com + DC=example, CN=demo + CN=example + CN=certificate
        Validity
            Not Before: Jan  1 00:00:00 2000 GMT
            Not After : Jan  1 00:00:00 2099 GMT
        Subject: DC=com + DC=example + DC=
-----BEGIN CERTIFICATE-----
MIIBgDCCATKgAwIBAgIUUZedX2PRkE2W95Tcf0KvyT89kVEwBQYDK2VwMF8xKjAR
BgoJkiaJk/IsZAEZFgNjb20wFQYKCZImiZPyLGQBGRYHZXhhbXBsZTExMAsGA1UE
AwwEZGVtbzAOBgNVBAMMB2V4YW1wbGUwEgYDVQQDDAtjZXJ0aWZpY2F0ZTAgFw0w
MDAxMDEwMDAwMDBaGA8yMDk5MDEwMTAwMDAwMFowXzEqMBEGCgmSJomT8ixkARkW
A2NvbTAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMTEwCwYDVQQDDARkZW1vMA4GA1UE
AwwHZXhhbXBsZTASBgNVBAMMC2NlcnRpZmljYXRlMCowBQYDK2VwAyEATX1Ud7XN
vdIv02Mf3gZSR+oFvhYEX3xWKamUUXThtK4wBQYDK2VwA0EA9QaR6uz/9i6dHKmP
v20IH22aLrczHybXTXpQ59zNKHEN69G2kBnd0ckV/WJ+bGCTvinulpOHY5SeJ64O
I7csAw==
-----END CERTIFICATE-----
, CN=demo + CN=example + CN=certificate
        Subject Public Key Info:
            Public Key Algorithm: ED25519
                ED25519 Public-Key:
                pub:
                    3e:8e:f0:91:09:07:b4:7b:df:f8:de:8d:a8:75:29:
                    a2:11:06:e3:38:88:a1:e0:b6:90:06:e7:61:ff:d7:
                    bc:50
    Signature Algorithm: ED25519
    Signature Value:
        16:fd:6c:6f:33:e3:4e:b4:94:1b:cf:d5:9a:0e:ce:4c:dd:47:
        65:36:a8:03:5b:a0:c5:04:ac:46:34:0c:2b:55:7b:3c:f8:e3:
        db:7a:f7:b4:77:f6:71:56:8a:fd:71:31:cb:25:62:a0:98:c5:
        2e:e4:e4:49:7b:eb:d9:43:c0:06
 

And the PEM certificate, which is much simpler:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            51:97:9d:5f:63:d1:90:4d:96:f7:94:dc:7f:42:af:c9:3f:3d:91:51
        Signature Algorithm: ED25519
        Issuer: DC=com + DC=example, CN=demo + CN=example + CN=certificate
        Validity
            Not Before: Jan  1 00:00:00 2000 GMT
            Not After : Jan  1 00:00:00 2099 GMT
        Subject: DC=com + DC=example, CN=demo + CN=example + CN=certificate
        Subject Public Key Info:
            Public Key Algorithm: ED25519
                ED25519 Public-Key:
                pub:
                    4d:7d:54:77:b5:cd:bd:d2:2f:d3:63:1f:de:06:52:
                    47:ea:05:be:16:04:5f:7c:56:29:a9:94:51:74:e1:
                    b4:ae
    Signature Algorithm: ED25519
    Signature Value:
        f5:06:91:ea:ec:ff:f6:2e:9d:1c:a9:8f:bf:6d:08:1f:6d:9a:
        2e:b7:33:1f:26:d7:4d:7a:50:e7:dc:cd:28:71:0d:eb:d1:b6:
        90:19:dd:d1:c9:15:fd:62:7e:6c:60:93:be:29:ee:96:93:87:
        63:94:9e:27:ae:0e:23:b7:2c:03

This makes it very obvious of what is happening, but it would also be possible to store the PEM certificate in the binary data of an X509 extension which would not be shown in such an explicit way by most tools.

Now the question is: Is there somewhere a system that includes two parsers that make different assumptions on the format of the certificate and could lead to making different interpretations, leading to a security vulnerability.

This post was updated to make it work with the openssl CLI, thanks to the idea to add the missing newlines by Richard Levitte