How a key transparency self-audit can verify the wrong identity
An encrypted email can arrive in the right inbox after someone else has already read it.
The cryptography is still doing its job. The mistake happens earlier, when the client receives the wrong public key.
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.
Thelemail has to solve public-key distribution too. A sender needs the correct key for the intended recipient. Give the sender a different one and the message is encrypted correctly for the wrong person.
In June, I came across a security write-up about a flaw reported through Proton’s bug bounty program . The write-up says Proton responded and paid a bounty. The review was possible because Proton’s clients are open source.
I went back to Thelemail and used the report as a checklist.
The report covers Proton’s implementation at that time. Proton currently documents key transparency as a beta feature that users enable in its web client. I have not verified whether the same flaw is still present.
The public key comes before encryption
Alice wants to send Bob an encrypted email.
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.
Encryption only knows which key it was given. It has no way to know whether that key belongs to Bob.
Alice sees nothing wrong when the provider returns an attacker’s public key. Her client encrypts the message with that key and sends it. The attacker decrypts it, reads it, then encrypts it again using Bob’s genuine public key.
Nothing looks unusual when Bob receives the 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
endOne way to prevent this is manual fingerprint verification. Alice and Bob compare their public-key fingerprints over another trusted channel and then pin the verified keys.
That works, but I do not expect people to compare fingerprints for every contact. Key transparency automates much of this checking.
A transparency directory records the keys associated with each identity. The client asks for a cryptographic proof that a key appears in the directory.
The client keeps a previous directory checkpoint and checks that the next one extends the same history. Removing or rewriting an old entry should then leave evidence.
The client still has to know which identity to 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 can expose a malicious key published under that identity.
In the reported implementation, the client asked the backend which email address to audit. Under a malicious-backend threat model, that gives the attacker control over the identity being checked.
Bob’s real address is bob@example.com, but the backend tells his 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 sees his genuine keys under other@example.com. The proof checks out, so the self-audit passes. It never looks at bob@example.com.
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.
Every proof verifies. Bob’s client still 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 identitiesAppend-only does not help here. The backend writes valid entries under different identities and never has to rewrite anything or produce a bad proof.
The audit identity must come from somewhere the backend cannot silently change.
Checking Thelemail against the same problem
Reading the report sent me back to Thelemail’s append-only public-key log .
The log does not store email addresses directly. It maps them 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 browser client and key transparency log server are open source. Thelemail also publishes an overview of the components and production revisions .
I can use these proofs to check inclusion and append-only history. They cannot tell the client which account identity to use for the VRF label.
I have not finished auditing that binding. I need to trace where the client first learns its account and mailbox identities, which parts it stores locally, and whether the backend can later change them without leaving evidence.
The proof can be perfect and the label can still be wrong.
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| SplitViewI kept these problems on separate branches in the diagram. A missing proof is different from a broken proof, and neither one tells me whether the client started with the correct identity. Split views need one more thing: someone independent has to compare signed log histories.
A failed proof currently does not block a message
I run Thelemail’s transparency checks in monitor mode today . The client verifies proofs and records failures, but still allows the message through. That helps while I test the implementation. It is not the final enforcement policy.
Some failures are ordinary outages: the client cannot reach the log service. Others are much harder to dismiss: a proof is cryptographically invalid or conflicts with a checkpoint the client already accepted.
The second case may mean that the log is lying, corrupted, or under attack. I do not want both cases hidden behind the same warning.
I could block every message when proof retrieval fails, but then a log outage stops mail. A warning that users can ignore is not much better for invalid proofs because most people will click through something they do not understand.
I have not settled the final behaviour yet.
I am leaning toward three states: verified, unavailable, and failed. A dangerous result should be difficult to ignore. A temporary outage should not stop email completely.
An append-only log can still show two histories
Thelemail has no independent witnesses yet . The threshold is currently zero.
A malicious log can give Alice one history and Bob another. Each history grows append-only, and both clients receive valid inclusion and consistency proofs. They still end up seeing different histories.
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 detectedAn independent witness has one job here: compare signed checkpoints seen by different clients. Two incompatible checkpoints are evidence that the log misbehaved.
Running several witnesses myself would make checkpoint collection more robust, but it would not add independent trust. I still need to find other people willing to run them, and I have not worked out how a small service should start using witnesses without pretending they are independent. If compromising Thelemail also compromises a witness operator, that witness adds very little.
What I am taking from this review
After this review, I still think Thelemail’s log and proof checks are worth keeping. They verify inclusion and append-only history, but I am not treating the work as complete.
The next checks are more specific:
- Trace the account and mailbox identity from registration through self-audit.
- Confirm that the backend cannot silently replace the identity used to derive the log label.
- Separate unavailable verification from invalid verification.
- Decide when the client must block sending.
- Add independent witnesses and require a non-zero threshold.
I am especially 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?
How do you bootstrap independent witnesses before there are outside organizations willing to run them?