TL;DR: My SSL certificate expiry check said github.com was covered until 11 Jan 2027. The ECDSA certificate github.com was actually serving expires on 29 Nov 2026, about six weeks earlier, and it also serves an RSA certificate that expires on 30 Dec 2026. My tool reads Certificate Transparency logs, and the logic picked the newest certificate in the log, one that was issued but not being served. A CT-based check should report the range of currently valid certificates and alarm on the earliest date, not the newest.
My SSL checker said github.com had a certificate valid until 11 Jan 2027. The certificate github.com was handing out expires on 29 Nov 2026.
Same hostname, about six weeks apart. One of those dates was wrong, and it was mine.
An SSL certificate expiry check that reports more time than you have fails quietly. You find out when the certificate does.
Where my dates come from
I built MyIPScan, a set of browser-based network and privacy tools. The SSL checker does not open a TLS connection to your server. Direct TLS inspection is not available in the runtime it runs in, so it reads public Certificate Transparency data instead: crt.sh first, Cert Spotter as a fallback.
CT logs are a public record of TLS certificates "as they are issued or observed" (RFC 6962, formally obsoleted by RFC 9162, which keeps the same wording). A CA may even submit a precertificate before the final certificate is issued (RFC 6962, Section 3.1). That is useful for auditing CAs. It says nothing about which certificate a server presents today.
I knew that. The logic still picked the newest certificate for the hostname and called it the answer.
Two explanations that don't fit
Stale CT data. CT can lag behind a fresh deployment, and the checker's own page says so. But lag would make the tool report an older certificate than the server's. Here the tool was ahead of the server: the log already held a certificate valid to 11 Jan 2027 that github.com was not serving.
One certificate, read wrong. If there were a single live certificate, there would be a single date to get wrong. There were two live ones.
The cause: two served certificates and a third in the log
These are the three certificates in the case:
| Certificate | Valid to | Days left (as of 22 Oct 2026) |
|---|---|---|
| ECDSA, served | 2026-11-29 | 38 |
| RSA, served | 2026-12-30 | 69 |
| Newest in CT logs, not served | 2027-01-11 | 81 |
Serving two certificates is a normal setup. A server keeps an RSA and an ECDSA certificate and picks one per connection. nginx, for example, lets ssl_certificate be specified multiple times "to load certificates of different types, for example, RSA and ECDSA" since version 1.11.0 (nginx docs). I am not claiming GitHub runs nginx; it is just the clearest documentation of the mechanism.
In TLS 1.3 the client sends a signature_algorithms extension (RFC 8446, Section 4.2.3), and the server chooses a certificate that fits it. Browsers offer both RSA and ECDSA, so which one you get is the server's preference. A client that only offers RSA signatures gets the RSA certificate.
The two served certificates were issued at different times, so they expire a month apart. A renewal was already in the logs. My tool skipped both served certificates and reported the one nobody was receiving.
How to check this yourself
You can ask for one key type at a time. These are the commands to run; I am not pasting output, because the dates will have moved by the time you read this:
# Offer only ECDSA signatures: expect the ECDSA certificate
openssl s_client -connect github.com:443 -servername github.com \
-tls1_3 -sigalgs "ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate -issuer
# Offer only RSA-PSS signatures: expect the RSA certificate
openssl s_client -connect github.com:443 -servername github.com \
-tls1_3 -sigalgs "rsa_pss_rsae_sha256:rsa_pss_rsae_sha384" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate -issuer
-sigalgs sets the list the client sends, and "the server selects one entry in the list based on its preferences" (openssl s_client docs). If the server holds two certificates, the two commands should print two different notAfter lines for one hostname.
Then compare them with the currently valid entries for the same name on crt.sh. It is common to find more valid certificates in the log than the server is using.
What an SSL certificate expiry check should report
There is no single expiry date for a hostname. There is a set of currently valid certificates, and the server serves some subset of them. Without a handshake you cannot know which subset.
So a CT-based check should do three things:
-
Report a range. Take every certificate for the exact hostname that is valid now and show the earliest and latest
not_after. For github.com on 22 Oct 2026 that is 29 Nov 2026 to 11 Jan 2027, not a confident 11 Jan. - Alarm on the earliest date. A newer certificate in the log only proves a renewal was issued, not that it was deployed. Until something confirms deployment, the earliest date is the one that can break you. The latest date goes next to it as "renewal logged".
- Count each certificate once. A precertificate and its final certificate are separate log entries with the same issuer and serial number.
A sketch of that logic:
const DAY = 86400000;
function expiryReport(certs, host, now = Date.now()) {
const valid = certs.filter(c =>
c.names.includes(host) && // exact match: *.github.com is not github.com
Date.parse(c.not_before) <= now &&
Date.parse(c.not_after) > now
);
// precertificate + final certificate = one certificate
const unique = [...new Map(
valid.map(c => [`${c.issuer}|${c.serial}`, c])
).values()];
if (unique.length === 0) return { status: "none" };
const days = unique
.map(c => Math.floor((Date.parse(c.not_after) - now) / DAY))
.sort((a, b) => a - b);
const earliest = days[0];
const latest = days[days.length - 1];
return {
earliest, // headline date
latest, // shown as "renewal logged"
renewalLogged: latest > earliest,
count: unique.length,
status: earliest <= 14 ? "alarm" : "ok",
};
}
This trades some false alarms for no silent misses. An old certificate that was replaced early but has not expired will keep the earliest date low. That is annoying, and it is the right direction to be wrong in. If you can open a TLS connection, use the handshake result to drop certificates the server no longer serves.
The range has a limit too. If the server serves a currently valid, logged certificate for this exact name, its date is inside the range. A misconfigured server serving something else is outside what CT can tell you.
What I changed
The checker's page now says it plainly: the records describe certificates that were logged, "not necessarily the one the server is serving right now", and nothing is read from a live handshake. It also tells you to re-check a few days after a renewal. The range view described above is the approach I am working toward; it is not on the page yet.
A range is less satisfying than one number. It is also not wrong.
The rule to take away
A certificate log tells you what was issued. Only a handshake tells you what is served. If your SSL certificate expiry check reads CT data, report the range across all valid certificates and alarm on the earliest one. If you can open a TLS connection, do it once per key type, because one hostname can have two expiry dates at the same time.
If you want to see what CT logs know about your own domain, the checker is on MyIPScan.
Top comments (0)