How a key transparency self-audit can verify the wrong identity
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
endManual 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 identitiesThe 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| SplitViewThe 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 detectedIndependent 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:
- 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 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:
References
Proton’s current key transparency documentation
Proton key transparency whitepaper
Thelemail security model and current limits