2026-07-27

It seems I restarted the TOFU war again (and my thoughts on it)

For the web folks, use the following link via mozz.us's web proxy

This post is going to be very rambly. TOFU has been a major point of discussion since I started my journey in the Geminispace. At that time I was also in support of TOFU to remove PKI as a Geminispace dependency. Back in early 2021 there were 2 major camps in support of TOFU (and Solderpunk's origin in the Gopherspace was a major influence, I believe, with the goal being making a better Gopher rather than to strip the web down to its bare essentials):

  • Those who wanted Gemini to be as decentralized as the early web (hence no CA, as they are a major point of failure)
  • Those who don't trust big tech and think CAs can be evil and could compromise Geminispace security

...the usual reaction to moving away from the web and removing its bloat. The points were valid at the time. But as of mid 2026, the situation has changed for me. AI has been getting uncannily better at catching bugs and hacking -- the problem has moved from whether CAs are evil to how resistant to MITM (and impersonation) attacks we are, and how to stop the bleeding after a compromise.

MITM first: people routinely lose their private keys to disk misfortunes and/or random lightning strikes. It's not uncommon to see a certificate warning when you visit a capsule after a few months. You are supposed to verify out of band whether the capsule has changed its certificate and not trust it until then -- but it doesn't work like that in reality. Where do you actually find the author to communicate with? What if it's just a routine certificate rotation due to expiration? Usually these are either not communicated at all or communicated directly on the capsule. Take a real example: sdf.org's certificate expires on 2026-10-07. On or near that day, sdf.org will have to serve a new certificate. What are people supposed to do if they visit sdf.org once per year and now find the cert has changed?

At most, people go on Station or bbs.geminispace.org to check if there's been a recent change, or more likely just accept the certificate and read the news on the capsule itself.

Which -- you can't distinguish between a routine certificate rotation and a MITM attack here. Someone owning a router between you and sdf.org can serve you a new certificate and just fake a news post on sdf.org saying "Hey, we just rotated our certs, don't worry about it!". An attacker can also time their attack to coincide with the real sdf.org's cert rotation to make it look like a routine change. At that point the only safeguard is to check the fingerprint against some web of trust or sneakernet, which I guarantee only the truly paranoid will actually do.

Let me share the shape of my mind using a single meme:

The huh cat
Image: The huh cat

No, that's a really bad security posture to be in. MITM was way too cost-prohibitive pre-AI to perform. You had to pwn routers and find a low-cost (performance-wise) way to attack and Gemini is a very low value target. No more of that with the advent of AI -- LLMs can find bugs faster than humans, thus lowering the economics needed to make an attack feasible.

To combat rotating certs semi-regularly (and out of reducing maintenance pain), capsules have opted to create long-living certs (10 years or even a century). This does remove the pain of rotation but introduces the problem of revocation. On the occasion of a compromise, the capsule can't really tell everyone that the certificate is now bad and clients must accept a new one. On the web this is done by 3 mechanisms: CRL, OCSP, and sheer certificate expiration. CRL and OCSP are CA-maintained lists of certificates that have been manually marked as not-trusted anymore; clients can check these lists to see if a certificate is still valid. For clients that opt not to implement these checks, the certificate expires in 3 months to a year, which naturally stops the bleeding.

This does not happen with long-living certificates. Your private key was stolen -- maybe via directory traversal or some other bug. What are you supposed to do? You wipe the server, reload the DB, create a new cert, and announce it on the BBS. A motivated and skilled attacker (which is almost everyone now -- just tell the AI to hack and do things) can poison the DNS or use some other method to impersonate your capsule under the now-compromised cert. Affected users see no reason to check sources about the attack as no certificate warnings show up on their client and have to rely on word of mouth -- which is slow, and by then who knows what the attacker has done.

Formally defining the attack vector

Let H be a capsule hostname, C0 its current certificate, K0 the corresponding private key, and F(C0) the certificate fingerprint stored by a client after a successful connection. Under TOFU, the client's only persistent authentication state is the assertion:

P[H] = F(C0)

On later connections, the client accepts a server claiming to be H when it proves possession of K0 and presents C0. The client warns only if the presented fingerprint differs from P[H]. This detects an unexpected certificate change, but it does not establish that the endpoint presenting the pinned certificate is still controlled by the capsule operator.

The relevant actors are:

  • O: the legitimate operator of H.
  • U: a client that has already pinned F(C0).
  • A: an attacker who can redirect U's connection to an attacker-controlled endpoint, for example through DNS poisoning or control of a router.

please formalize the attack vector into the following paragraph

The post-compromise attack requires two conditions:

  • A obtains K0, and normally C0 as well, through a server compromise, backup leak, directory traversal bug, or another disclosure.
  • A can cause U's connection for H to terminate at A's endpoint.

O may detect the compromise, remove the vulnerable server, and deploy a replacement certificate C1 with private key K1. A then performs the attack as follows:

  • U attempts to connect to H.
  • A redirects that connection to A's endpoint.
  • A presents C0 and proves possession of K0 during the TLS handshake.
  • U computes F(C0), finds that it equals P[H], and accepts the connection without a certificate-change warning.
  • A serves arbitrary content as H and can falsely claim that no compromise occurred, suppress O's recovery notice, or direct U to trust attacker-controlled replacement credentials.

The attack therefore succeeds when:

redirect(U, H, A) AND knows(A, K0) AND P[H] = F(C0)

The failed security property is endpoint authenticity: U completes an apparently authenticated connection for H, but the peer is A rather than O. TLS remains cryptographically sound in this model; A is accepted because A possesses the very credential that U was taught to trust.

There is no in-band recovery from this state. A notice served from H is not authoritative because A can forge or suppress it, while C1 appears to U as an unexpected certificate and C0 does not. The impersonation window lasts until U learns out of band that C0 was compromised, manually replaces the pin, or the client rejects C0 for some independent reason such as certificate expiry. A long-lived certificate consequently turns a brief key disclosure into a potentially long-lived impersonation capability.

A related attack exists without key theft during first contact or certificate rotation. If P[H] is unset, or U expects the certificate to change, A can redirect the connection and present an attacker-generated certificate CA. On first use the client records F(CA); during rotation the warning is expected and may be accepted. In either case TOFU can record the attacker's key as the new trusted state. The key-compromise variant is stronger because an already-pinned client receives no warning at all.

The certificate-change warning also presents U with an ambiguity that TOFU cannot resolve. Suppose U has pinned F(C0) but is now presented with a different certificate Cx. At least two worlds are consistent with that observation:

  • Wrotate: O intentionally replaced C0 with C1, perhaps because C0 expired, was lost, or was compromised. In this world Cx = C1, and accepting the change restores access to the legitimate capsule.
  • Wattack: A redirected the connection and presented CA. In this world Cx = CA, and accepting the change pins the attacker's certificate.

From U's point of view, both worlds produce the same local evidence:

P[H] = F(C0) AND F(Cx) != P[H]

In other words, the client knows that the certificate changed but does not know whether the change was intentional. The TLS transcript and the TOFU database contain no additional fact that distinguishes Wrotate from Wattack. Rejecting Cx is safe against substitution but may reject a legitimate rotation; accepting Cx restores availability after a legitimate rotation but may complete the attack.

Checking H for an explanation does not resolve the ambiguity, because the explanation is delivered over the connection whose identity is in question. Both O and A can serve a convincing notice claiming that the certificate was intentionally rotated. U therefore needs evidence obtained through an already-trusted independent channel -- such as a previously authenticated signing key, an out-of-band fingerprint, or a trusted third party -- to determine which world it is in.

Gemini CA?

Hopefully I have convinced you that TOFU as it currently stands is not viable long term. However, the Gemini spec is intentionally frozen and near-impossible to extend in an effort to combat protocol bloat. Anything proposed has to be either compatible with the existing spec or an optional extension that runs asynchronously and doesn't break the baseline specification.

Before discussing that, one critical point to share is that ACME with some modification can work on Gemini via Spartan. Unfortunately that means we do need a client that speaks an alternative protocol to produce a signed certificate for Gemini's use. And that at most gets us back to having a CA (but at least one that isn't run by big tech).

Now, onto if we want to have a new CA. Maybe? But what does a Gemini CA actually solve versus just using Let's Encrypt or many of the other free CA options out there? I assume the main thing would be the ability to obtain a certificate with smol-net-only technology (i.e. without HTTP), which does fit the self-sufficient ethos. But there's no more security benefit to having a Gemini CA versus the established ones. Plus security is serious business, and at least the established CAs have been around for a while and have a lot of experience operating them. And mutual auditing (even if potentially that's more BS than anything else).

A Gemini-specific CA would also have to run on small infra. More likely some VPS or a Raspberry Pi somewhere. Given the low attack value of Gemini, that should be fine, but still, it doesn't sit right with me.

On the plus side, a Gemini CA does solve the problem of revoking certificates, is compatible with the existing spec (you just add new root certs to Gemini clients), and TOFU can be kept as it doesn't care whether the cert is signed by whoever. Recovering from a compromise also has a standard playbook of just re-issuing the cert. Which, if the attacker only has read access, they cannot do anything about.

Pros:

  • We all know how this is supposed to work, with a well established playbook and a lot of experience

Cons:

  • Huge infra
  • Complex integration and clients needs to add new root certs
  • Same CA-induced centralization that early Gemini tried to avoid

Community maintained CRL?

This is another idea I have (quite literally during a shower). What if we had a community-maintained CRL?

Periodically, a client supporting the CRL extension would fetch a list of revoked certificates from a trusted community source (there could be many operated by different people). Once a capsule owner realizes their cert has been compromised, they can submit a revocation request to the community CRL. Protocol as follows:

statement = encode(
    "revoke-certificate-v1",
    maintainer_identifier,
    SHA256(certificate_DER),
    nonce,
    timestamp
)

signature = Sign(certificate_private_key, statement)

This allows the CRL service to identify that SOMEONE (not necessarily the capsule owner) holding the private key is requesting the revocation of the certificate. At this point browsers should distrust the revoked certificate and warn the user when a new certificate is used on the capsule, as revocation only requires the private key -- the browser cannot automatically trust the new certificate, since the attacker can also issue a revocation and, if they own a MITM or impersonation attack, can present a new certificate by then.

That is a bit dissatisfying. However, the CRL service cannot hold a "next certificate" field without more authentication, as it does not know whether the capsule owner or the attacker is the person submitting the revocation request.

The solution would be, like ACME, to have a pre-shared secret between the capsule and the CRL service. Then, assuming the attacker does not have root access, the capsule owner can prove to the CRL service that they are the owner of the capsule and can submit a new certificate to be trusted. Due to the self-signing nature (instead of the CRL service signing the certificate, like a CA), the CRL service must check the certificate's not-before date against the secret creation date to ensure the secret was not created after the compromise. The secret could potentially be tied to the common name to reduce the risk of it leaking (and remove the need to check for creation time).

Which, I think, might be the right thing to do.. Validation and challenge/response would be a registration process and proving capsule ownership by putting a piece of text file on the capsule.

Registration

 Capsule owner                 CRL service                  Capsule
       |                            |                           |
       |-- register hostname ------>|                           |
       |<-- challenge + nonce ------|                           |
       |                            |                           |
       |-- publish challenge file ----------------------------->|
       |                            |                           |
       |                            |-- fetch challenge file -->|
       |                            |<-- challenge + nonce -----|
       |                            |                           |
       |                            |-- verify hostname,        |
       |                            |   challenge, and nonce    |
       |<-- revocation secret ------|                           |
       |                            |                           |
       |-- store secret separately from the TLS private key     |
       |                                                        |
       v                                                        v


Later, after some unfortunate event

 Capsule owner                 CRL service                    Clients
       |                            |                            |
       |-- revocation request ----->|                            |
       |   + revocation secret      |                            |
       |   + compromised cert hash  |                            |
       |   + replacement cert       |                            |
       |                            |                            |
       |                            |-- verify secret matches    |
       |                            |   the hostname and predates|
       |                            |   the replacement cert     |
       |                            |                            |
       |                            |-- add old cert to CRL ---->|
       |                            |-- publish new  cert       >|
       |                            |                            |
       |                            |                    reject old cert
       |                            |                    trust replacement

Which does make the CRL services a MITM target for obtaining the revocation secret. However this could be mitigated by publishing and sharding the certificate fingerprint everywhere on Geminispace and let TOFU do it's job properly.

Pros:

  • Self-signed cert works as is
  • Clients stay simple and no CA integration is needed
  • Clients ought to not support the scheme stays as so
  • Revocation and successor stops MITM/impersonation after discovery
  • The CRL can disappear if no one uses it and nothing changes even for who adopted it

Cons:

  • Introduce some level of centralization
  • The shared secret cannot be leaked or else the attacker can revoke the capsule's certificate (same as any CA with ACME)