Expect-CT Lite: A humble proposal for minimal CT enforcement in TLS certificates

Last week we witnessed the discovery of the compromise of an XMPP service, most likely by a Western nation-state. While it's not directly related to any attack which was carried out in this incident, one thing that has been brought to my attention in the aftermath of this is that Certificate Transparency logging remains optional for CAs.

Background: Certificate Transparency. For those unfamiliar, Certificate Transparency is a system which allows an append-only, cryptographically authenticated public audit log of issued TLS certificates to be maintained for public inspection. The idea is that when a CA issues a TLS certificate, that certificate, in addition to being provided to the party that requested it, is also submitted to several transparency logs operated by different third parties. The idea is that if a CA does issue a certificate it's not supposed to, there is a chance for people to find out about it. (A consequence of this is that all such certificates — basically any certificate issued for any website, ever — can be viewed in a public ledger; this is not considered an issue as this information is considered public.)

Certificate Transparency has seen great uptake and is now mandatory in some cases. In particular, it's possible to generate a piece of data known as a Signed Certificate Timestamp (SCT), which is basically a cryptographic proof that a certificate was sent to a given CT log by a CA (these SCTs are signed by a CT log's private key, so only the CT log operator can generate them).

Since then, many web browsers (Chrome, Safari) have come to mandate the use of SCTs, and will now generally refuse to accept a web server's TLS certificate which doesn't come with a proof that it has been sent to at least two known CT logs.

Gaps in the CT landscape. However, it turns out there are a couple of gaps in the CT landscape, namely these:

  • Inconsistent client-side CT enforcement. While web browsers enforce and require proof of submission to a CT log, most other client applications using TLS don't. This is a product of the defaults in most TLS libraries; no TLS library that I'm aware of will require a CT proof by default. There are various reasons (discussed subsequently) why turning this on by default might prove difficult. Since not requiring a CT proof is the default, it's the inevitable result that most applications that aren't web browsers will leave this default as it is and naturally not require a CT proof either.

    This means that while a malicious, malfunctioning or compromised CA could not mis-issue a certificate and have Chrome accept it without logging that certificate publicly (ensuring we find out about it), it could do so for the majority of non-web-browser HTTP clients and the majority of non-HTTP application protocol clients — which of course includes services such as XMPP.

    Surprisingly, Firefox has yet to enforce CT.

  • Logging not required. CAs still aren't required by the CA/Browser Forum Baseline Requirements (the rules which CAs are required to follow, lest they face the CA Death Penalty) to log certificates to CT logs. A CA can issue a certificate “secretly” even today and not be breaking any rules.

    While CAs today generally do log certificates to public CT logs by default, in some circumstances they may not. For example, some customers may believe in security by obscurity (ahem) and not want certain certificates to be visible in the public CT logs, and specially request the CA issue a certificate which is not logged. Some CAs are known to provide this service on request. These certificates aren't useful with web browsers since web browsers now require proof of submission to a CT log before they will accept a certificate, but, due to the above situation where most non-web-browser application clients don't enforce this, they can be readily used with other services.

    The issue here is not just or even primarily malicious or compromised CAs (if a CA is malicious or compromised, it obviously can't be trusted to follow the rules, so making it a rule that CAs must log to CT wouldn't help here), but that if a malicious third party can trick a CA into issuing a certificate for a domain, he can also request the CA doesn't log that certificate, and not having that certificate be logged benefits the attacker, as it reduces the chance of his illicitly obtained certificate being discovered.

    When you combine this with the above situation where non-web applications generally do not enforce CT logging cryptographically, you have a situation where a MitM-capable attacker in the vein of the recent XMPP.ru compromise could conceivably request an unlogged certificate from a CA, use their MitM capabilities to exploit the DV loophole and validate their control of the victim domain, and illicitly obtain a certificate without it being logged. While this certificate would not be useful against web browsers, which require a certificate to have proof of submission to a CT log, the above issue means that other services, such as XMPP services, as well as most HTTP-based services where the client is not a web browser, could in general be readily compromised by a fraudulently obtained certificate without that certificate ever being logged.

Note that while the risk of a malicious or compromised CA not logging certificates is real, and better client-side CT enforcement would mitigate this, in this article I want to focus on the situation where the CA is entirely legitimate and functioning correctly, but has been successfully fooled by a third party into issuing a certificate for a given domain, and honours a request by that customer not to log the certificate. I'll call this the “gullible CA” scenario.

The current state of handling of the “gullible CA” scenario above is not good as a fraudulently obtained certificate can go unlogged and be successfully used by an attacker. There are various possible solutions to this status quo:

  1. Rules changes. The first possible fix is for the CA/Browser Forum to vote to adopt a change to the Baseline Requirements to require CT logging for all certificates.

  2. Better SCT enforcement for non-web clients. The second possible fix is for non-web client applications to more frequently and uniformly require TLS certificates to come with proof of having been logged to CT.

Since (1) involves politics, there are probably obstacles to achieving it, most obviously the likelihood that there are many parties that like the idea of being able to get certificates that aren't logged publicly.

However, (2) also comes with some problems, as this HN comment by agwa illustrates, which I will quote here:

I wrote briefly about the challenges faced by non-browser clients at https://news.ycombinator.com/item?id=36436303 but the tldr is that all SCT-checking clients have to be able to respond to ecosystem changes very quickly. This is so important that Chrome and Safari both disable SCT checking entirely if the browser hasn't been updated in more than 10 weeks.

and:

Hopefully, although there are challenges to overcome. CT is a fast-moving ecosystem, with logs coming and going, and policies changing regularly. This requires CT-enforcing clients to be very on-the-ball with updates, both in the sense that the developers need to pay attention and update their code in time, and any users of the apps need to upgrade frequently. Browser makers can handle this because they are competently-staffed and well-resourced. The authors of non-browser apps need to know what they're getting into.

A cautionary tale: there is a library for adding CT enforcement to Android apps. Earlier this year, every app using this library was suddenly unable to establish any TLS connections because Google stopped publishing a JSON file which the library should never have been consuming in the first place. There was plenty of warning that this would happen, but the author of the library was not on-the-ball.

In short, doing full cryptographic validation of SCTs as a client is high maintenance and quite brittle. Even browser vendors have some concerns about this and have designed their SCT validation to automatically be bypassed if a browser isn't updated for a number of weeks; their confidence in their ability to do SCT validation resiliently comes from their ability to rapidly push updates.

The underlying problem here is that there is an entire infrastructure for distributing the list of trusted root CAs as determined by the Mozilla, Microsoft, Apple, etc. root CA programs. Every operating system, every Linux distribution, comes with one of these lists and the infrastructure to keep it updated, so software can reasonably count on it being up to date and correct; after all, nothing will work right if it isn't. But validating CT signatures requires also maintaining a list of known CT logs and their public keys in an analagous way to the root CA list, and there is currently a lack of stable infrastructure for distributing and updating this list, which has the potential to lead to breakage if strict CT enforcement is attempted. A client application developer which wants to do their own CT enforcement at this point is basically forced to homegrow their own infrastructure to manage this, and the maintenance burden and update channels needed to ensure it continues to function correctly. Needless to say, most application developers aren't going to do this.

Expect-CT Lite. In this article, I'd like to propose a novel third option, which I'm going to coin “Expect-CT Lite”. This idea is capable of mitigating against the “gullible CA” scenario, in many cases without changes to existing deployed infrastructure. Only changes to application clients are needed, and these changes are much simpler and lower-maintenance than the changes needed to enable a client for full SCT enforcement in the vein of web browsers. In particular, there is no need for a client to maintain or update a list of known CT logs.

First, some necessary background. A SCT (a proof that a certificate has been logged) can be distributed to a client connecting to a TLS server in three different ways:

  1. by incorporation directly into the certificate issued by a CA, as an X.509 extension containing the SCT;
  2. via an OCSP response from a CA's OCSP server, as an extension containing the SCT;
  3. via a TLS extension added by the TLS server, which contains the SCT.

(3) is not very popular as it requires TLS server operators to configure it specially. By comparison, (1) works “automatically” when enabled by a CA for all the certificates it issues, without server operators having to do anything in particular. The same is also true of (2) if a server operator has already deployed OCSP stapling (or if a client is willing to fetch OCSP responses itself).

Expect-CT Lite is a proposal which is capable of functioning in conjunction with methods (1) or (2). Since these deployment channels taken together are overwhelmingly the most popular deployment channels, this isn't much of a limitation. (Indeed, any deployment of (3) is likely bootstrapped via (1) or (2) in the first place anyway.)

The idea of Expect-CT Lite is beautifully simple, which I'll demonstrate assuming we are using case (1). Let's take the TLS certificate for this website, which is issued by Let's Encrypt. Here's an extract from the certificate:

    X509v3 extensions:
        [...other extensions omitted...]
        CT Precertificate SCTs: 
            Signed Certificate Timestamp:
                Version   : v1 (0x0)
                Log ID    : 7A:32:8C:54:D8:B7:2D:B6:20:EA:38:E0:52:1E:E9:84:
                            16:70:32:13:85:4D:3B:D2:2B:C1:3A:57:A3:52:EB:52
                Timestamp : Aug 21 09:19:31.387 2023 GMT
                Extensions: none
                Signature : ecdsa-with-SHA256
                            30:46:02:21:00:8F:2D:89:60:98:A2:C5:56:F5:CC:B3:
                            F4:12:01:10:C7:2F:47:AE:5A:77:3D:83:19:81:81:15:
                            EB:BE:B5:2D:07:02:21:00:D8:5B:87:87:3D:E8:9C:82:
                            FE:59:80:13:48:65:4D:2F:88:12:60:44:86:16:CA:FA:
                            E2:5D:EC:CD:11:7E:2A:19
            Signed Certificate Timestamp:
                Version   : v1 (0x0)
                Log ID    : AD:F7:BE:FA:7C:FF:10:C8:8B:9D:3D:9C:1E:3E:18:6A:
                            B4:67:29:5D:CF:B1:0C:24:CA:85:86:34:EB:DC:82:8A
                Timestamp : Aug 21 09:19:31.435 2023 GMT
                Extensions: none
                Signature : ecdsa-with-SHA256
                            30:45:02:21:00:85:85:04:1F:3A:18:7A:2D:D3:28:21:
                            30:FD:0D:7F:9B:E6:95:57:CE:82:13:C0:C9:F9:FD:9C:
                            8D:E5:92:60:37:02:20:54:A7:FF:35:90:F8:89:E4:35:
                            EA:33:22:8C:CF:B4:D7:88:2D:0A:F0:8A:04:E7:02:CA:
                            AA:35:77:81:06:2B:E3

The idea of Expect-CT Lite is simply this: rather than doing the full cryptographic validation of these SCTs, a client can simply assume that a certificate was logged if this extension is present. The reason is that this extension is inside the certificate, meaning that it is actually signed by the CA — so it is essentially the CA attesting that it has logged the certificate.

Of course this does not do anything against a malicious or compromised CA. This is why each SCT has its own signature from the CT log operator — so that you don't have to take a CA's word for it that a certificate was logged. However, as we have noted above, validating these SCTs cryptographically requires a client to maintain a list of known CT logs and keep it updated, which is a substantial burden. Unlike the Mozilla and other root CA programs, which distribute lists of trusted root CAs, comparable infrastructure for distributing trusted CT log lists is lacking. Giving application clients without the budget for sophistication (and continual maintenance) of web browsers a way to get some validation has the potential to be the difference between a client doing any level of CT validation and no validation at all.

The idea is that the presence of the SCT extension in the signed portion of a certificate, while it does not protect against a compromised CA, does protect against the “gullible CA” scenario in which a CA has been persuaded by a malicious third party into issuing an unlogged certificate. An application client can use the absence of the SCT extension to determine if the CA has issued the certificate as an unlogged certificate. A legitimate CA can then be induced to issue an unlogged certificate in accordance with its own policies, and as it is allowed to do so under the present rules, but such a certificate will not be accepted by an application client configured in this way to accept only certificates bearing a SCT extension.

The actual contents of the SCT extension is completely ignored and not cryptographically validated (as that would require maintaining a list of known CT logs). Essentially, the presence or absence of the SCT extension is taken as expressing a single bit of information: did the CA decide to log this certificate or not? The CA's word is taken as good, and is protected by the CA's signature over the entire certificate. This is not as good as full SCT validation because it cannot mitigate against a malicious or compromised CA, but it is better than the status quo of the long tail of non-web-browser application clients not doing CT validation at all, because it can mitigate against the “gullible CA” scenario.

OCSP deployment. While the use of the SCT extension inside a certificate is the preferred scenario for our “Expect-CT Lite” idea here, OCSP deployment is also viable. The use of the SCT extension inside a CA certificate is slightly irksome to some CAs since it requires issuing and signing a “precertificate”, logging that to the CT log and getting a SCT back from the log service, then creating a real certificate containing that SCT (needed because a certificate obviously cannot contain a SCT of itself). This doubles the number of signing operations needed. While services such as Let's Encrypt accept this burden, some CAs may choose to distribute SCTs separately via their OCSP responder endpoint instead (though it seems that this approach is increasingly unpopular in favour of the certificate-based approach; indeed, I am unaware of any CAs still currently using this approach).

Since OCSP responses are also signed by a CA, there is similarly no issue with a client checking for a SCT extension inside the OCSP response, which it is hopefully already in the habit of verifying. As usual, OCSP responses can be fetched by a client or stapled by a server to avoid the need for the additional round trip by the client. The mechanism of checking whether a SCT extension is present is essentially the same. If either a certificate or a signed OCSP response contains a SCT extension, and the CA is trusted, it can be assumed that the CA has logged the certificate. The signatures over the certificate and/or OCSP response, of course, chain to root CA certificates in one of the public root certificate list programes (e.g. the Mozilla, Microsoft or Apple programs). Thus, this validation step leverages the existing infrastructure for distributing, managing and updating trusted root CA lists and does not require deployment of a new list of trusted public keys (CT logs) and the supporting management infrastructure.

Example code: OpenSSL. Example application code which an application using OpenSSL can use to implement “Expect-CT Lite” is available here.

Future directions: standardisation. The premise of this article is to use the presence or absence of an SCT extension inside a certificate signed by a CA as a reliable indicator of whether an assumed-trustworthy CA logged a certificate or not. A future alternative would be to standardise an X.509 policy OID which explicitly represents a CA's decision on whether to log a certificate. This could be a bit more flexible since it could be used by a CA to communicate either the fact of having already logged a certificate or an intention to log a certificate in the future (whereas a SCT proves a certificate has already been logged).

Essentially you would have a policy OID which represents “intend to log”, meaning that a CA promises (on their honour, or a revised set of Baseline Requirements) to log a certificate to CT before or promptly after a certificate is issued; as well as an OID meaning “not going to be logged” indicating a CA has made an explicit decision not to log a certificate, perhaps due to a customer request. An absence of either OID would indicate ambiguity, and that a CA is not communicating anything about CT logging. If adoption of such a policy OID eventually became required, clients could start requiring it if they did not want to accept unlogged certificates.

Still, while this is theoretically a cleaner approach than taking the presence of a SCT extension as a cue of a CA's decision to log a certificate, getting adoption on this (to a point where CAs could be made to adopt it universally) would take quite some time, in practice the latter seems practical enough. The interesting thing about this proposal, Expect-CT Lite, is that it can literally be deployed today to start securing applications against unlogged certificates and the risk they pose.

Suggestions for applications. If you develop an application which uses TLS, you might consider adopting this approach. If you do, you should give users a way to turn it off, as people should have a choice as to whether they want to accept unlogged certificates or not. (Consider organisation-internal root CAs, etc.)

Feedback. Feedback on this proposal (or any other of my writings) is always welcome.

If you previously read my article on the XMPP.ru MitM incident, you might want to read my followup article with XMPP-specific discussion.