Blog - 0x00


Exploring TLS certificates and their limits

2024/07/13

Table of contents


#

While setting up the server for my pet adoption platform I had to configure TLS and used certbot to get a Let’s Encrypt certificate. While this was easy to do, one “annoying” thing about Let’s Encrypt certificates is that they are valid for only 90 days and the way I had setup my server I couldn’t find an alternative way to auto-renew the certificates, so I had to login to the server and do the process manually every 90 days.

I thought to my self it would be nice if the certificate lasted more than 90 days and wondered whats the longest a certificate could last, how long can fields in certificate be? In this blog I’ll try to explore, test and learn more about TLS certificates.

About TLS certificates #

It’s important to first know that certificates have a format defined by the standard X.509 (public key certificates) which is why sometimes reading about certificates you’ll read them as “X.509 certificate”. Though from my understanding X.509 just defines the public key certificates structure while RFC 5280 define their structure AND usage on the Internet for TLS. I read and followed what RFC 5280 said.

A TLS certificate of 0x00.cl

As you can see in the image above, a TLS certificate has several fields, some describe the validity, the algorithm used for the public key but there are other fields that are simply “informational” such as Issuer Name which can be customized when creating a certificate. There are other tabs such as “ISRG Root X1” and “R11” those are the root and intermediate certificates. “ISGR Root X1” issued a certificate called “R11” and using “R11” they issued a certificate to my website “0x00.cl” this is a chain of trust, if your browser or client can trust the root certificate such as “ISGR Root X1” then it should also trust the certificates issued by it. Browsers like Chrome and Firefox use the Common CA Database (ccadb) to include Certificate Authorities (CAs) in their browsers.

You can create your own root certificate and be your own CA BUT because the certificate isn’t going to be included in any software, most if not all software will either throw an error or at least a warning about the certificate not being trusted. badssl is a website that shows examples of bad and good certificates.

Creating certificates #

The first thing we need to do is to create a certificate, this will be the root certificate.

I’ve had created certificates before using openssl (OpenSSL 3.2.1) and when you lookup on how to create them, usually websites will use openssl, as its a command line tool easy to work with and installed with most linux distributions. First you have to create a key:

$ openssl genrsa -out myCA.key 2048

So whats the 2048 argument? Its the size of the key in bits specifically because the subcommand genrsa generates an RSA private key. This subcommand is a “shortcut” to generating a private key using the subcommand genpkey. The equivalent would be:

$ openssl genpkey -algorithm RSA -out myCA.key -pkeyopt bits:2048

Once you have a key, you can create the certificate using the req subcommand:

$ openssl req -x509 -new -noenc -key myCA.key -sha256 -days 3650 -out myCA.pem

This creates a certificate that will last 10 years (3650 days). The final two files:

$ ls -l
-rw-------. 1 tomas tomas 1704 Jul  3 12:00 myCA.key
-rw-r--r--. 1 tomas tomas 1237 Jul  3 12:00 myCA.pem

Key size #

Because creating a key is the first step to creating a certificate I thought, what if I set the number of bits to a higher number, instead of using 2048 use 65536

$ openssl genrsa -out myCA.key 65536
Warning: It is not recommended to use more than 16384 bit for RSA keys.
         Your key size is 65536! Larger key size may behave not as expected.

I let it run for like 30 minutes but decided to stop as it was taking too long and didn’t look like it would finish any time soon. For comparison 2048 bits takes ~250ms, also openssl throwing a warning didn’t look promising, so I settled with 16384 (it took 30 seconds).

$ ls -l
-rw-------. 1 tomas tomas 12632 Jul  3 12:00 myCA.key

Well, thats about 12kB for the key, but like I mentioned before genrsa subcommand specifically generates an RSA key but what about other algorithms to create a key? Would they output a bigger key? So I first checked which options were available (RSA, RSA-PSS, EC, X25519, X448, ED25519 and ED448.) and tested them but RSA was the only one I could get to output a bigger key in bytes, also changed the hashing algorithm from SHA256 to SHA3-512 as it should output more bits.

Again, I create the certificate using this new private key and get:

$ ls -l
-rw-------. 1 tomas tomas 12632 Jul  3 12:00 myCA.key
-rw-r--r--. 1 tomas tomas  6091 Jul  3 12:00 myCA.pem

Perfect, now the certificate went from 1.2kB to 6kB just because the private key was created using more bits (16384).

Certificate Fields #

Certificate have several fields that provide information not only about the certificate but also about the issuer and subject of the certificate. RFC 5280 section 4.1.2.4 defines attributes for the issuer field that MUST be present.

Until now when we created the certificates all those fields had been left with default values, so this is another opportunity to make the certificate bigger by adding long strings.

$ openssl req -x509 -new -noenc -key myCA.key -sha3-512 -days 3650 -out myCA.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:CC
State or Province Name (full name) []:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Error making certificate request
800275933F7F0000:error:06800097:asn1 encoding routines:ASN1_mbstring_ncopy:string too long:crypto/asn1/a_mbstr.c:106:maxsize=128

Ok, so I can’t just add super long strings, there is a limit in this case for the “state or province name” the max length is 128… but what about the other fields? Well, sadly for us there are defined upper bounds in RFC 5280 Appendix A.1 (which openssl defines in crypto/asn1/tbl_standard.h).

Attribute Upper bound
Country name alpha 2
Organization name 64
Organizational unit name 64
State or province name 128
Common name 64
Locality name 128

It’s possible to use the -subj flag to pass these values instead of doing it interactively like when I first created it.

$ openssl req -x509 -new -noenc \
    -key myCA.key \
    -sha3-512 \
    -days 3650 \
    -out myCA.pem \
    -subj "/C=XX/ST=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/L=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/O=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/OU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/CN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/emailAddress=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$ ls -l
-rw-------. 1 tomas tomas 12632 Jul  3 12:00 myCA.key
-rw-r--r--. 1 tomas tomas  7550 Jul  3 12:00 myCA.pem

Well, I managed to increase it from 6kB to around 7.5kB, not a lot. I’ll try to “cheat” and check if I can repeat some attributes, I’m going to be using the same command as above but the string in -subj will be copy pasted several times. I won’t but it here as it will be too long and unnecessary, but you get the idea.

$ ls -l
-rw-------. 1 tomas tomas  12632 Jul  3 12:00 myCA.key
-rw-r--r--. 1 tomas tomas 249845 Jul  3 12:00 myCA.pem

Woah! Almost 250kB for a certificate. Now that we know the “secret” to make it as big as we want, we can move on to the valid period of the certificate.

Valid period #

When I first created a certificate using openssl you can define in days how long should the certificate last, by default the notBefore field is set to the current date and from the current date it calculate the date for notAfter based on the days given.

I wanted to test what was the max value for the notAfter. So the first thing I tried was setting the number of days to a big number.

$ openssl req -x509 -new -noenc \
    -key myCA.key \
    -sha3-512 \
    -days 36500000000 \
    -out myCA.pem
req: Value "36500000000" outside integer range
req: Use -help for summary.

Well, I guess setting a certificate that is valid for 100 million years is too much. I’ll use a smaller number, like 3650000 (10000 years).

$ openssl req -x509 -new -nodes \
    -key myCA.key \
    -sha3-512 \
    -days 3650000 \
    -out myCA.pem

Looks like it worked! I’ll check the information of the certificate

$ openssl x509 -noout -text -in myCA.pem    
Could not open file or uri for loading certificate from myCA.pem: No such file or directory
$ ls -l myCA.pem
ls: cannot access 'myCA.pem': No such file or directory

Hmm… openssl complains about the number being too big and when its small enough for openssl it doesn’t create the certificate without throwing any error or warning. I tested and made sure it wasn’t a problem with the command by using a smaller number of days and it did create the certificate, so the number of days was the problem.

When I read RFC 5280 section 4.1.2.5, a certificate accepts two time formats, “YYMMDDHHMMSSZ” and “YYYYMMDDHHMMSSZ”. It is implicitly written in RFC 5280 that dates can range from 1950 to 9999, so that is going to be our limits.

Now that I can technically set notAfter to 9999 by setting the correct amount of days, what about notBefore? Is it possible to set a value in the past for the certificate? Does openssl allow that?

The openssl req or openssl x509 commands don’t allow to set dates explicitly for certificates but openssl ca does allow to sign certificates and customizing the start date and end date fields. After trying for a while to generate one, the only way I found I could use that command is that you must already have a CA certificate with a Certificate Signing Request (CSR)

First I have to create a CSR:

$ openssl x509 -in myCA.pem -signkey myCA.key -x509toreq -out myCA.csr

So I can create a customized certificate:

$ openssl ca -startdate '19500101000000Z' -enddate '99991231235959Z' -in myCA.csr -keyfile myCA.key -cert myCA.pem -out myCA.crt
Using configuration from /etc/pki/tls/openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number:
            01:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
        Validity
            Not Before: Jan  1 00:00:00 1950 GMT
            Not After : Dec 31 23:59:59 9999 GMT
        Subject:
            countryName               = XX
            stateOrProvinceName       = Mandatory
            organizationName          = Default Company Ltd
            commonName                = 0x00.cl
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Subject Key Identifier: 
                7E:F9:D5:33:0E:B8:64:DF:19:94:AB:59:23:A0:0A:55:D0:40:C3:B0
            X509v3 Authority Key Identifier: 
                7E:F9:D5:33:0E:B8:64:DF:19:94:AB:59:23:A0:0A:55:D0:40:C3:B0
Certificate is to be certified until Dec 31 23:59:59 9999 GMT (2912992 days)
Sign the certificate? [y/n]:y

Great the certificates have the minimum and maximum dates, but there is one thing that is bothering me, is that with this command I’m creating a new certificate and the CA certificate still has the old dates set. I could continue trying with openssl to create such certificate and maybe self sign the CA but it was already getting way too complicated for me. Not only I had to figure out the proper way to use the command options and flag but also when using the command to set custom dates I had to set up openssl.conf with directories and files. So I moved on to Python and create the certificates programmatically using the package Cryptography.

This is the repository with the code: https://gitlab.com/0x00cl/tlscertlimits

It was far easier, here is how I created a private key and certificate for the CA.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import datetime
from pathlib import Path

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.x509.oid import NameOID

ca_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=16384,
)

with Path.open("certs/myCA.key", "wb") as f:
    f.write(
        ca_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        ),
    )

ca_subject = ca_issuer = x509.Name(
    [
        x509.NameAttribute(NameOID.COUNTRY_NAME, "WW"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "W" * 128),
        x509.NameAttribute(NameOID.LOCALITY_NAME, "W" * 128),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "W" * 64),
        x509.NameAttribute(NameOID.COMMON_NAME, "0x00.cl CA root"),
    ]
)

ca_cert = (
    x509.CertificateBuilder()
    .subject_name(ca_subject)
    .issuer_name(ca_issuer)
    .public_key(ca_key.public_key())
    .serial_number(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0)
    .not_valid_before(datetime.datetime.fromisoformat("1950-01-01 00:00:00.000+00:00"))
    .not_valid_after(datetime.datetime.fromisoformat("9999-12-31 23:59:59.000+00:00"))
    .add_extension(
        x509.BasicConstraints(ca=True, path_length=None),
        critical=True,
    )
    .add_extension(
        x509.KeyUsage(
            digital_signature=True,
            content_commitment=False,
            key_encipherment=False,
            data_encipherment=False,
            key_agreement=False,
            key_cert_sign=True,
            crl_sign=True,
            encipher_only=False,
            decipher_only=False,
        ),
        critical=True,
    )
    .add_extension(
        x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()),
        critical=False,
    )
    .sign(ca_key, hashes.SHA3_512())
)

with Path.open("certs/myCA.pem", "wb") as f:
    f.write(ca_cert.public_bytes(serialization.Encoding.PEM))

It was a lot easier, specially for the attributes where you can just “multiply” the characters instead of having to write them all out. Let’s examine the certificate we just created.

$ openssl x509 -noout -text -in certs/myCA.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0f:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:f0
        Signature Algorithm: RSA-SHA3-512
        Issuer: C=WW, ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, CN=0x00.cl CA root
        Validity
            Not Before: Jan  1 00:00:00 1950 GMT
            Not After : Dec 31 23:59:59 9999 GMT
        Subject: C=WW, ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW, CN=0x00.cl CA root
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (16384 bit)
                Modulus:
                    00:ce:5e:d0:f9:e1:76:71:b2:13:20:29:2b:a6:35:
                    0a:39:77:56:a7:06:39:5a:82:d6:92:72:bb:9c:9a:
                    41:84:32:b5:31:01:67:c0:01:f6:ad:bd:d2:72:89:
                    # ... cut 134 lines
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Subject Key Identifier: 
                9F:CC:F1:B4:36:98:8B:8C:24:CB:0A:A2:F5:50:5C:5A:3A:AE:64:14
    Signature Algorithm: RSA-SHA3-512
    Signature Value:
        b4:5a:57:72:68:59:85:a4:da:85:6f:f5:e8:db:44:01:a2:08:
        21:de:ac:55:1b:c3:cb:51:4e:61:fd:2b:e5:93:78:45:62:b2:
        af:3e:47:4b:fe:71:15:bb:37:c1:80:fb:a7:4d:49:d1:f2:85:
        # ... cut 111 lines

Note: The certificates “Modulus” is 137 lines long and the “Signature Value” is 114 lines long, they just were too long and not relevant so I cut them.

Perfect, we have the minimum and maximum values for a valid certificate date.

I also created an intermediate certificate and finally one for a website. Because It was done programmatically its a simple as multiplying fields and lists.

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
web_subject = x509.Name(
    [
        x509.NameAttribute(NameOID.COUNTRY_NAME, "WW"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "W" * 128),
        x509.NameAttribute(NameOID.LOCALITY_NAME, "W" * 128),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "W" * 64),
        x509.NameAttribute(NameOID.COMMON_NAME, "0x00.cl Web"),
    ]*4000
)

web_cert = (
    x509.CertificateBuilder()
    .subject_name(web_subject)
    .issuer_name(inter_cert.subject)
    .public_key(web_key.public_key())
    .serial_number(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2)
    .not_valid_before(datetime.datetime.fromisoformat("1950-01-01 00:00:00.000+00:00"))
    .not_valid_after(datetime.datetime.fromisoformat("9999-12-31 23:59:59.000+00:00"))
    .add_extension(
        x509.SubjectAlternativeName([x509.DNSName("localhost")]),
        critical=False,
    )
    .sign(int_key, hashes.SHA3_512())
)

The resulting files:

$ ls -l myCA*
-rw-r--r--. 1 tomas tomas      12632 Jul 03 12:00 myCAinter.key
-rw-r--r--. 1 tomas tomas       7034 Jul 03 12:00 myCAinter.pem
-rw-r--r--. 1 tomas tomas      12632 Jul 03 12:00 myCA.key
-rw-r--r--. 1 tomas tomas       6977 Jul 03 12:00 myCA.pem
-rw-r--r--. 1 tomas tomas      12632 Jul 03 12:00 myCAweb.key
-rw-r--r--. 1 tomas tomas 1067089731 Jul 03 12:00 myCAweb.pem

Great we ended up with a 1GB file. Though I must say that generating a list with 4000 x509 attributes in Python did end up using a lot of RAM. I first tried 40000 but crashed because it ran out of memory and with 4000 it barely made it and ended up using almost all 16GB of RAM in my computer.

Serving certificates #

Now that we have created a big certificate, we need to serve them and see how a web browser behaves. I decided to use Caddy (v2.8.4) as a web server just because I have never tried it before and wanted to test it out. Here is the Caddyfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
	debug
	http_port 8080
	https_port 8443
	local_certs
	skip_install_trust
}

localhost {
	tls ../certs/myCABundle.pem ../certs/myCAweb.key
	respond "Hello, World!"
}

I downloaded the binary and simply ran it like this:

$ ./caddy run -c Caddyfile

It took around 20 seconds for Caddy to start but it worked, no errors thrown.

Clients #

I first tried using a web browser (Firefox) but when I tried loading the website (https://localhost:8443) I kept getting the error PR_END_OF_FILE_ERROR.

Firefox PR_END_OF_FILE_ERROR message

The logs in caddy don’t show an ERROR but they do show a DEBUG log where there is a TLS handshake error message coming from crypto/cryptobyte/builder.go, though not really sure why is the error and why its classified as DEBUG and not ERROR, maybe the error is on the client side.

2024/07/03 12:00:00.000	DEBUG	http.stdlib	http: TLS handshake error from 127.0.0.1:42146: cryptobyte: pending child length 788004684 exceeds 3-byte length prefix

I tried curl (8.6.0) too but that didn’t work either.

$ curl -kv https://localhost:8443                       
* Host localhost:8443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8443...
* Connected to localhost (::1) port 8443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8443 
* Closing connection
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8443

At first I thought maybe I didn’t create the certificates properly but because I knew I was trying to load a 1GB certificate (which at best they are a few kBs) I tried with smaller one and that did work. I tried again loading the big certificate but again same error. Maybe there is a limit to how big a certificate can be. I tried reducing the amount of fields to reduce the size of the certificate to a few MBs but that didn’t work either, so I slowly reduced the size of the certificate until I got a new error.

The size of the new certificate was 1.1MB. Using firefox I got the error SSL_ERROR_RX_MALFORMED_HANDSHAKE.

Firefox SSL_ERROR_RX_MALFORMED_HANDSHAKE message

Also the “error” on caddy was different and with curl too.

2024/07/11 15:50:29.281	DEBUG	http.stdlib	http: TLS handshake error from 127.0.0.1:47612: remote error: tls: error decoding message
$ curl -vk https://localhost:8443
* Host localhost:8443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8443...
* Connected to localhost (::1) port 8443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (OUT), TLS alert, illegal parameter (559):
* OpenSSL/3.2.1: error:0A000098:SSL routines::excessive message size
* Closing connection
curl: (35) OpenSSL/3.2.1: error:0A000098:SSL routines::excessive message size

At least curl now shows an actual message for the error which is “excessive message size”. Looks like curl doesn’t like “big” certificates and there is a limit to it. Since curl knew that the certificate was too big, then there must be a limit coded or configured so I started looking at the source code in git and found in the manpages that the maximum certificate chain size is 100kB. From what could I understand from the source code this is defined in ssl/statem/statem_local.h (102400 bytes = 100kB).

So I tried to make it as big as openssl allowed it (100kB) by reducing the amount of repeated fields to 221 and it worked:

$ curl -vk https://localhost:8443 
* Host localhost:8443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8443...
* Connected to localhost (::1) port 8443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl Web; C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl Web; C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl Web; C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl Web; C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl Web; C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
*  start date: Jan  1 00:00:00 1950 GMT
*  expire date: Dec 31 23:59:59 9999 GMT
*  issuer: C=WW; ST=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; L=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; O=WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW; CN=0x00.cl CA Intermediate
*  SSL certificate verify result: self-signed certificate in certificate chain (19), continuing anyway.
*   Certificate level 0: Public key type RSA (16384/272 Bits/secBits), signed using RSA-SHA3-512
*   Certificate level 1: Public key type RSA (16384/272 Bits/secBits), signed using RSA-SHA3-512
*   Certificate level 2: Public key type RSA (16384/272 Bits/secBits), signed using RSA-SHA3-512
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://localhost:8443/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: localhost:8443]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.6.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: localhost:8443
> User-Agent: curl/8.6.0
> Accept: */*
> 
< HTTP/2 200 
< alt-svc: h3=":8443"; ma=2592000
< content-type: text/plain; charset=utf-8
< server: Caddy
< content-length: 13
< date: Fri, 03 Jul 2024 12:00:00 GMT
< 
* Connection #0 to host localhost left intact
Hello, World!

By using the flag --trace-ascii - in curl we can get more information about the exchange of data.

$ curl -k --trace-ascii - https://localhost:8443
...
== Info: TLSv1.3 (IN), TLS handshake, Certificate (11):
<= Recv SSL data, 102404 bytes (0x19004)
...

It received 102404 bytes for the certificate which is just 4 bytes off from the limit found in source code of openssl, not sure exactly how either curl or openssl is calculating those bytes, but even adding only only 1 character (which should be 1 byte) makes curl (openssl) throw error.

Something interesting I found was that I tried using Firefox to make a request but it threw an error message, different from the previous ones SEC_ERROR_BAD_DER, although it was working with curl. Thats when I started reading a bit more and found that Firefox uses nss, though I couldn’t find where they set the limit for the certificate size it seems to limit the size to the extensions (sections) of the certificate. For example the subject of the certificate I could multiply the values 221 times for curl before it started throwing errors while Firefox only allowed 111 times BUT the field “SubjectAlternativeName” I could “extend” it at least 1000 times and Firefox would still work, even though it had already reached the limit in size for the subject fields.

Final thoughts #

Exploring TLS certificates was certainly fun, I did learn quite a bit about them specially because I had to read several times RFC 5280 but also a bit sad that although a 1GB certificate could technically work, in the end its going to be limited by the clients such as your web browser or CLI tools like curl. I did expect to be able to do something “crazier” such as being able to use negative dates for example or a date past 9999 years, which for example openssl did allow to set a date lower than 1950 such as 0000 but that wouldn’t conform to the RFC 5280 standard and the python package cryptography does set a limit to 1950.

In the introduction of this blog I wondered:

And hopefully it was also easy for you to understand and also learned something about TLS certificates from this blog. :)