How a key transparency self-audit can verify the wrong identity

By ·

An encrypted email can arrive in the correct inbox and still have been readable by someone else.

The encryption algorithm does not need to fail. The client only needs to select the wrong public key before encryption starts.

I am building Thelemail , private email hosting for people and small teams using their own domains. Stored mail remains encrypted, and readable private-key material stays on each user’s device.

Public-key distribution is part of that design. A sender needs the correct key for the intended recipient. If the server supplies a different key, the message can be encrypted correctly for the wrong person.

In June, I read a security write-up about a flaw reported through Proton’s bug bounty program. The write-up says Proton responded and paid a bounty. Proton’s clients being open source made the review possible.

I used the report as a checklist for Thelemail.

The Proton implementation described here is the one covered by the June report. Proton currently documents key transparency as a beta feature that users enable in its web client. I am not claiming that the same flaw is still present.

The public key comes before encryption

Suppose Alice wants to send encrypted mail to Bob.

Alice’s client asks the provider for the public key belonging to bob@example.com. The provider returns Bob’s public key, and Alice encrypts the message with it. Bob then uses his private key to decrypt the message.

The encryption knows which key it received. It does not know whether the key belongs to Bob.

If the provider returns an attacker’s public key, Alice can still encrypt and send the message without seeing an error. The attacker decrypts it, reads it, then encrypts it again using Bob’s genuine public key.

Bob receives a normal encrypted message.

sequenceDiagram
    autonumber

    participant Alice as Alice's client
    participant Directory as Key directory
    participant Attacker as Attacker or malicious provider
    participant Bob as Bob's client

    Alice->>Directory: Request key for bob@example.com

    alt Honest response
        Directory-->>Alice: Bob's public key K_B
        Alice->>Bob: Encrypt with K_B and send
        Bob->>Bob: Decrypt with private key SK_B
    else Malicious response
        Directory-->>Alice: Attacker's public key K_A
        Alice->>Attacker: Encrypt with K_A and send
        Attacker->>Attacker: Decrypt with private key SK_A
        Attacker->>Bob: Re-encrypt with K_B and forward
        Bob->>Bob: Decrypt with private key SK_B
    end

Manual fingerprint verification can prevent this. Alice and Bob compare their public-key fingerprints over another trusted channel and then pin the verified keys.

That works, but people are unlikely to compare fingerprints for every contact. Key transparency tries to perform much of this checking automatically.

A transparency directory records the keys associated with each identity. The client can request a cryptographic proof showing that a key appears in the directory.

The client can also keep a previous directory checkpoint and verify that a newer checkpoint extends the same history. This makes it difficult for the directory to remove or rewrite old entries without detection.

There is still a separate question: which identity should the client check?

The self-audit checked the wrong entry

Proton’s key transparency design includes a self-audit.

The client checks that the directory contains the correct public keys for its own email address. This is meant to detect a malicious key published under the user’s identity.

In the reported implementation, the client obtained the email address for this audit from the backend.

That becomes a problem when the backend itself is the attacker.

Suppose Bob’s real address is bob@example.com. The backend tells Bob’s client to audit other@example.com.

The backend places Bob’s genuine public keys under other@example.com. It then places an attacker’s public key under bob@example.com.

Bob’s client checks other@example.com, finds Bob’s genuine keys, verifies the proof, and reports that the self-audit passed.

Alice asks for the key belonging to bob@example.com. She receives the attacker’s key and a valid proof showing that this key is recorded under Bob’s real address.

Both clients receive valid proofs. Bob’s client simply audited the wrong identity.

sequenceDiagram
    autonumber

    participant Bob as Bob's client
    participant Backend as Malicious backend
    participant Log as Transparency directory
    participant Alice as Alice's client

    Note over Backend,Log: other@example.com maps to Bob's genuine key K_B<br/>bob@example.com maps to attacker's key K_A

    rect rgb(230, 247, 235)
        Bob->>Backend: Which address should I audit?
        Backend-->>Bob: other@example.com
        Bob->>Log: Prove keys for other@example.com
        Log-->>Bob: K_B with a valid proof
        Bob->>Bob: Self-audit passes
    end

    rect rgb(255, 235, 235)
        Alice->>Backend: Request key for bob@example.com
        Backend-->>Alice: Attacker's key K_A
        Alice->>Log: Verify K_A for bob@example.com
        Log-->>Alice: Valid inclusion proof
        Alice->>Backend: Message encrypted with K_A
        Backend->>Backend: Decrypt using SK_A
        Backend->>Bob: Re-encrypt with K_B and deliver
    end

    Note over Bob,Alice: Every proof can be valid while the clients verify different identities

The append-only property does not stop this attack. The backend does not need to rewrite an existing entry or produce an invalid proof.

It only needs to arrange valid entries under different identities.

The identity being audited must come from somewhere the backend cannot silently change.

Checking Thelemail against the same problem

Thelemail already has an append-only public-key log.

Email addresses do not appear directly in the log. They are mapped to opaque labels derived with a verifiable random function, or VRF.

The client requests an inclusion proof to confirm that a key was recorded under a label. It also checks consistency proofs between log checkpoints. A consistency proof shows that a new version of the log extends the previous version instead of replacing its history.

The client and log server are open source.

These checks cover inclusion and append-only history. They do not decide which account identity the client should use when deriving the VRF label.

That identity binding needs its own audit.

I need to trace where the client first learns its account and mailbox identities, which parts are stored locally, and whether the backend can later change them without leaving evidence.

A client can verify an inclusion proof perfectly and still verify the wrong label.

flowchart TD
    Identity["Account or mailbox identity"]

    Binding{"Is the identity bound<br/>independently of the backend?"}

    Wrong["Risk: the client may<br/>verify the wrong label"]

    VRF["Derive opaque VRF label"]
    Log["Append-only public-key log"]

    Inclusion["Verify inclusion proof"]
    Consistency["Verify consistency proof"]

    Result{"Verification result"}

    Verified["Verified"]
    Unavailable["Verification unavailable"]
    Failed["Verification failed"]

    Checkpoint["Signed log checkpoint"]
    Witnesses["Independent witnesses"]
    Threshold{"Witness threshold met?"}
    SplitView["Split views are not<br/>automatically detected"]

    Identity --> Binding
    Binding -->|No or unknown| Wrong
    Binding -->|Yes| VRF

    VRF --> Log

    Log --> Inclusion
    Log --> Consistency

    Inclusion --> Result
    Consistency --> Result

    Result -->|Proofs valid| Verified
    Result -->|Proof cannot be fetched| Unavailable
    Result -->|Invalid or inconsistent proof| Failed

    Log --> Checkpoint
    Checkpoint --> Witnesses
    Witnesses --> Threshold
    Threshold -->|Current threshold: 0| SplitView

The diagram includes three separate problems because combining them into one success or failure flag would hide useful information.

The client needs to know which identity it is checking.

It also needs to distinguish an unavailable proof from an invalid one.

Finally, someone independent needs to compare signed log histories.

A failed proof currently does not block a message

Thelemail currently runs its transparency checks in monitor mode.

The client verifies proofs and records failures, but it does not block a message when verification fails. This is useful while I am testing the implementation. It is not a final enforcement policy.

There are at least two different failure cases.

The client may be unable to fetch a proof because the log service is temporarily unavailable. It may also receive a proof that is cryptographically invalid or inconsistent with a checkpoint the client has already accepted.

The first case may be an availability problem. The second may be evidence that the log is lying, corrupted, or under attack.

A single generic warning would make those cases look the same.

Blocking every message whenever proof retrieval fails gives the log service the power to stop mail during an outage. Allowing the user to ignore an invalid proof weakens the protection because most people will click through warnings they do not understand.

I have not settled the final behaviour yet.

The client will probably need separate states for successful verification, unavailable verification, and failed verification. The dangerous state must be difficult to ignore without turning every temporary outage into a complete loss of email availability.

An append-only log can still show two histories

Thelemail does not have independent witnesses yet. The current witness threshold is zero.

Without witnesses, a malicious log can show one internally consistent history to Alice and another internally consistent history to Bob.

Both histories can be append-only. Both clients can receive valid inclusion and consistency proofs for the history they were shown.

They simply do not see the same history.

sequenceDiagram
    autonumber

    participant Alice as Alice's client
    participant Log as Malicious log
    participant Bob as Bob's client
    participant Witness as Independent witness

    Log-->>Alice: Signed checkpoint C_A
    Log-->>Bob: Signed checkpoint C_B

    Alice->>Log: Request consistency proof from previous checkpoint
    Log-->>Alice: Valid proof within history A

    Bob->>Log: Request consistency proof from previous checkpoint
    Log-->>Bob: Valid proof within history B

    Note over Alice,Bob: Alice and Bob each see a consistent history<br/>but C_A and C_B are incompatible

    Alice-->>Witness: Publish or report C_A
    Bob-->>Witness: Publish or report C_B

    Witness->>Witness: Compare signed checkpoints
    Witness-->>Alice: Conflicting histories detected
    Witness-->>Bob: Conflicting histories detected

Independent witnesses compare signed checkpoints observed by different clients. If a log signs incompatible histories, the conflicting checkpoints become evidence that the log misbehaved.

Several witnesses operated by Thelemail could make checkpoint collection more robust, but they would not add independent trust. The difficult part is finding other parties willing to operate them and deciding how a small service should introduce witnesses without pretending they are independent.

Protocol diagrams tend to show three or five witness boxes as if they appear automatically. In practice, each witness needs an operator independent enough that compromising Thelemail does not also compromise the witness.

What I am taking from this review

The current Thelemail log and proof verification are still useful. They provide a base for checking inclusion and append-only history.

I am not treating the work as complete.

The next checks are more specific:

  1. Trace the account and mailbox identity from registration through self-audit.
  2. Confirm that the backend cannot silently replace the identity used to derive the log label.
  3. Separate unavailable verification from invalid verification.
  4. Decide when the client must block sending.
  5. Add independent witnesses and require a non-zero threshold.

I am interested in how other small systems have handled the last two points.

How do you enforce proof failures without allowing a temporary log outage to stop the whole product?

And how do you bootstrap independent witnesses before there are outside organizations willing to run them?

The current implementation and its limitations are documented at:

https://thelemail.com/keys

References

June 2026 security write-up

Proton’s current key transparency documentation

Proton key transparency whitepaper

Thelemail key transparency

Thelemail security model and current limits

Thelemail open-source components

← all posts