Originally introduced in 1.19.1, chat reporting has undergone many changes in Mojang's attempts to eliminate the exploits and make the system functional. The purpose of this paper is to document the current technical state of chat reporting on an ongoing basis, and to provide a reference for the community to use when discussing the system. To that end I will try to keep it as unbiased as possible.
Chat reporting heavily relies on cryptographic commitments and signatures to ensure that reported chat messages are not tampered with. The basic idea is that all players sign their chat messages with their private key, and then send the signature along with the message to the server.
Chat signing keypair is not generated by the client as one could expect; instead, it is issued by Mojang's services and is tied to the player's account. This means that the keypair is shared between all clients that the player uses, and the player can't change it. The keypair is fetched from Mojang's services when the client logs into any server that supports message signing (currently offline-mode servers don't).
Public key from this keypair is signed with Mojang's own private key, along with the player's UUID and expiration date. This signature is obtained along with chat signing keypair and can be verified using their public key, which is available as part authlib, to ensure that the keypair was indeed issued by Mojang. The expiration date is used to invalidate the keypair after a certain amount of time, and is currently set to 48 hours. When the keypair expires, the client will fetch a new one from Mojang's services.
Public key along with Mojang's signature is sent to the server when the player joins, and is used to verify the signatures of all chat messages that the player sends. The server also relays public key and signature to all other players, so that they can verify the signatures that player's messages as well.
Actual algorithm used for chat signing is Java's default implementation of SHA256withRSA
. Mojang's signature is generated using SHA1withRSA
. Key sizes are 2048 bits and 4096 bits respectively.
So what exactly constitutes a "message" in this system? Believe it or not, it's more than just the text that the player typed. Let's take a look at a JSON that represents one:
{
"index": 3.0,
"profileId": "bfa45411874a4ee0b3bd00c716059d95",
"sessionId": "79930ef9b869476db5c5d297884432f5",
"timestamp": "2022-10-30T17:32:44.160Z",
"salt": 5.1050580637282796E18,
"lastSeen": [ "RSA Signature 1", "RSA Signature 2", "RSA Signature 3" ],
"message": "How are you",
"signature": "RSA Signature"
}
This is an almost perfect reflection of message format used in actual chat reports. Let's go over the fields one by one:
-
index
is a number that is incremented by 1 every time the player sends a message within the same chat session. -
profileId
is the player's UUID. -
sessionId
is a unique ID of chat session. It is generated by the client when the player joins the server. We will discuss chat sessions in more detail in a later section. -
timestamp
is the time when the message was sent. It is generated by the client. -
salt
is a random number that is generated by the client. In earlier versions of the system it was used to prevent replay attacks, but they are no longer possible due to how the system works now. It's current purpose remains unknown. -
lastSeen
is an array of RSA signatures of up to 20 most recent messages that the player has seen, including their own. -
message
is the text of the message. -
signature
is the RSA signature of the message. It is generated against all the data in fields above using player's private key.
Probably the most important part of the message is that lastSeen
array. It is relied upon to establish what messages were seen by the the player at the moment they have sent their own.
Every time a message is sent, client goes over 20 last messages in the chat, from most to least recent. For every message that is considered "seen" by player - its signature is added to the list of last seen messages.
Obvious reason to implement something like this is the existence of private messages; just because the message was sent to the server does not mean all clients will receive it, so this list helps establish who exactly received what. However, that another player's message is received by the client still does not neccessarily constitute it being seen by the player. For instance, they might block another player via Social Interactions screen, in which case that player's messages won't be displayed in chat screen, and subsequently will not be marked as seen.
By default, server will also perform some amount of validation on last seen signatures, to ensure that client can't suddenly "unsee" previously seen messages, make last seen signatures up, and that their order is compliant with chronological order of actual chat log.
Worth noting that in the context of chat reports, record of last seen messages of reported person must be taken at face value. Otherwise it becomes possible to incriminate anyone by fabricating arbitrary conversation around their messages and pretending that they simply refused to admit to seeing fabricated messages.
Another thing worth noting is that signatures in the last seen lists aren't actually directly sent to the server; instead, client sends a bit set that informs the server which messages in the chat log it should take the signatures from. Likewise, server may not always send signatures directly to the client, and can instead supply message index that the client uses to look up the message in question in previously received chat log. This is simply an optimization to avoid sending unnecessary data around, as signatures themselves are quite long.
Chat sessions are used to keep track of whether client currently signs their messages or not, ensure their order, and indicate when it switches to a new signing keypair whenever old one expires. Vanilla client will automatically try to start chat session immediately after logging into the server; however, this is not enforced by vanilla server. Without starting chat session client can only send unsigned messages.
As soon as chat session is started, server will expect all player's messages to be signed going forward, and trying to send an unsigned one will get the player kicked. The only way to terminate chat session on vanilla server is to leave the server.
The process of obtaining chat signing keypair and starting a chat session is illustrated in the following flowchart:
Each chat session has a UUID associated with it, which is picked by client when starting a new session. Message indexation also depends on chat session - every time new session is started, client will begin indexing its messages from 0. This indexation is validated by the server. Clients will also try to validate indexation of other clients, but since their knowledge about total chat log is limited - the only thing that's checked is that index of any new message from the player is greater than the index of last message received from them.
Accounting for everything we know by now, we can now draw another flowchart that illustrates the entire process of sending a message:
Note that the server relays the message back to sender as well, so that they can see their own message in chat and know that it has been delivered successfully. The sender will then perform all the same checks on this message as it does on messages from other players.
Whenever the client's current chat signing keypair is close to expiring, it will fetch new one from Mojang's services and start a new chat session, sending public part of the keypair to the server as part of chat session update packet. Keypair change is a necessary condition for vanilla server to allow chat session update; otherwise it will simply ignore the packet. Vanilla clients, however, impose no such restriction on chat session updates for other clients, which can in principle be leveraged to implement custom functionality such as arbitrary chat session termination if the server is modified.
To briefly mention how exactly Minecraft treats messages sent via commands such as /tell
and /msg
- it does so via "signed arguments", which contain message text and are essentially the same as signed messages. Each signed argument increments indexation and is signed along with the same data as normal messages, and server will perform all of the same checks on them. Signed arguments of all vanilla commands are eventually relayed to other players as a type of chat message.
Note that as far as signed data is concerned - there is no distinction between private and public messages, so it's not possible to tell them apart in the context of chat reports.
To encapsulate what we have learned so far, let's take a look at a conversation between three players: Alice, Bob and Mark. It is represented by the following flowchart:
Chronological order is left to right. Rectangular boxes represent private messages and are colored according to the receiver, while ellipses represent public messages. Arrows show which messages are included in the last seen list associated with each message, and numbers on them indicate the position of the message in that list, from most to least recent. Each message also has an index as shown above the message.
Note that indexation is individual to each sender, and that last seen list of Mark's only message includes only one public message from Bob, as the rest was privately exchanged between Alice and Bob.
There are a few ways in which chat signing manifests itself in-game. First of all are visual indicators that messages are decorated with in client's chat screen, collectively referred to as "Chat Trust Status":
-
Unsigned messages
Unsigned messages have a light gray bar to the left of the message:
Hovering over it reveals following tooltip:
-
Modified messages
Modified messages are those that are modified by the server in a known way, i.e. the server still supplies original message text against which message signature can be verified. They have a dark gray bar to the left of the message:
Hovering over them displays gray question mark icon which can be hovered to see original message text:
-
System messages
System messages are not signed by any player, but are instead generated by the server. They have a light gray bar to the left of the message:
Hovering over it reveals following tooltip:
Another thing is "Only Show Secure Chat" option available in Chat Settings:
When enabled, it will prevent any unsigned or modified messages from being displayed in chat.
As you may imagine, chat reporting would not be particularly effective if anyone could simply opt out of signing their messages. There are two main ways in which Mojang currently enforce chat signing:
-
On servers that have both
online-mode
andenforce-secure-profile
set totrue
inserver.properties
, chat signing is enforced by the server itself. This is the default configuration for vanilla servers. The server will refuse to deliver any unsigned messages. -
Vanilla client will automatically start chat session upon joining any server, and sign all messages it sends, as long as it is able to fetch chat signing keypair from Mojang's services. There is no way to opt out of this behavior without modifying the client. The only exception is when client joins a server that does not set up encrypted connection, which is the case for servers that have
online-mode
set tofalse
inserver.properties
. In this case, client will not attempt to start chat session, and will send all messages unsigned.
While disabling enforce-secure-profile
will allow players to send unsigned messages, it will not remove signatures from messages that are sent with such anyways. This means that chat reporting cannot be disabled on a per-server basis without modifying the server software.
Now we can finally examine how chat reporting itself works. Part of this process that end user is faced with is not particularly convoluted, and involves simply going through a few GUI screens.
It begins by clicking "Report Player" button next to any player in Social Interactions screen:
This will open the following screen:
Here you can add optional description of the incident. You will also have to select messages to be reported by clicking on "Select Chat Messages to Report" button, which takes you to the following screen:
Here you can select reportable messages by clicking on them. Messages that can not be reported are grayed out (such as your own). Up to 4 different messages can be selected.
Unreportable messages include unsigned and system messages. A player can only be reported if they have sent at least one signed message.
Note the "1 message(s) hidden" text at the bottom of the screen. Messages are hidden if they will not be included in the report evidence due to context selection rules. We will discuss this in more detail later.
Once you are done, clicking "Done" button returns to the previous screen.
Report category also has to be selected, which is done by clicking on "Select Report Category":
Each category here comes with a short description, which you can read by clicking on it. Once you are done, clicking "Done" button returns to the previous screen.
Finally, you can click "Send Report" button to submit the report. If the report is successfully submitted, you will be taken to the following screen:
What actually happens behind the scenes is a POST
request to https://api.minecraftservices.com/player/report
, where request body is a JSON object containing all of the report details. Few important things to note here are:
-
The request is sent to Mojang's servers, not the server that the incident took place on. This means that the server cannot see the report, and cannot prevent it from being submitted.
-
Since client is responsible for sending the report, it is also responsible for providing the context surrounding reported messages.
-
Sending the request requires authorization with valid Minecraft access token, which means that reports are not anonymous.
Let's take a look at the actual report payload intercepted in 1.19.3 using Report Debug Tool. It's quite long, so I have posted it as a separate Gist: https://gist.github.com/Aizistral/33dd257068a7ded8707185dd61ce149e
Actual conversation it represents proceeded as follows:
Note that unsigned and system messages are not included in the report evidence. This is because they are not signed, and therefore cannot be verified.
The fields in the report payload are:
-
version
Presumably this is the version of report/signature format. Currently it will be always set to1
. -
id
UUID of the report, randomly generated by the client. -
report
-
opinionComments
Report description provided by the user. -
reason
Report category selected by the user. -
evidence
messages
An array of messages containing both messages selected by the user and messages that were automatically included as surrounding context. Each message contains the exact set of fields as shown in the Message Structure section, with an addition ofmessageReported
field, which is set totrue
if the message was selected by the user for reporting, andfalse
otherwise. For obvious reasons this field is not a part of signed message data.
-
-
clientInfo
clientVersion
This is the version of the client that the report was submitted from. It will also have "(modded)" appended to it if the client is modded, although nothing in particular prevents a mod from removing this suffix.
-
thirdPartyServerInfo
Only included if the incident took place on third party server.serverAddress
IP address of the server.
-
realmInfo
Only included if the incident took place on Minecraft Realms.-
realmId
UUID of the realm. -
slotId
Slot ID of the realm.
-
So what exactly determines whether a message will be included in the report evidence? Context selection rules used to be quite convoluted in 1.19.1/1.19.2, but were simplified significantly in 1.19.3. For every message reported, client iterates over entire preceeding chat log in chronological order, and includes any message that is either:
-
Contained in the last seen list of reported message;
... and/or ...
-
Sent by the same player as the reported message, and within the same chat session.
Selection stops as soon as it finds 9 such messages or runs out of chat log. This process runs individually for each reported message, and resulting message lists are merged excluding duplicates. This means that if a player reports 4 messages, and each of them has 9 preceeding messages, the resulting report will contain up to 40 messages.
Since client is the one responsible for sending the report, measures are necessary to prevent it from tampering with the evidence. This is what this whole chat signing affair is about. While we cannot know how exactly reports are validated on Mojang's end, there is a lot we can deduce from data integrity of which is ensured by message signatures.
The most obvious attack that chat signing prevents is forging incriminating messages on behalf of reported player. Since each message is signed by players using their own private keys, an adversary can't forge the signatures as well. This also prevents them from modifying the message contents, since associated signatures would no longer be valid.
Back in early 1.19.1 snapshots this was the only type of attack signature system was designed to prevent. However, it was quickly discovered that it was possible to tamper with the context of the conversation. Malicious reporter could simply remove messages from the chat log, or inject arbitrary messages from themselves that were never even sent to the server, as they know their own private key and can generate a valid signature whenever.
This is how last seen message list was introduced. It represents player's commitment to seeing messages from other players. Each signature in the last seen list is unique to all message data that it was generated against, and since the list itself goes into player's own signature - it's not possible to modify any messages from this list, including adversary's own messages.
This is largely what drives context selection rules - if the message is not from a reported player and was not seen by a reported player, it shouldn't be included in the report in the first place, and it's presence there is an immediate indication of tampering. This ensures that forged messages cannot be injected into the report, which eliminates a great deal of potential attacks.
Role of indexation in this system is supplemental. It can be used to deduce how many messages from a particular player is missing from the report, if any, although nothing can be established about the content of those messages or reason for their absence. Last seen lists can as well be used to gain some limited information about missing messages, mostly constrained to their exisence.
There are a few legitimate reasons why messages might be missing from the report, such as:
-
An error occured on the server while processing the message, and it was never relayed to any players;
-
The message was private and was never relayed to the reported player;
-
The player has blocked the person who sent the message.
These reasons can be leveraged by an adversary to hide any messages from the report, as we will discuss in the next section. However, it will still be visible that some messages are missing, whether that makes the report more suspicious or not.
As wise person once said:
Any person can invent a security system so clever that she or he can't think of how to break it.
This particular system also takes upon itself a great commitment: to ensure that not only individual messages can't be tampered with, but also that the context of the conversation can't be changed, all without trusting either the server or the client. This is a very ambitious goal, and it's not surprising that it has not enjoyed total success. In fact, it has failed spectacularly when the system was initially introduced in 1.19.1 snapshots, which caused Mojang to delay the release of the feature for a month.
You can take a look at the video I have released back then to see what exploits were possible at the time: https://www.youtube.com/watch?v=JVpfqXxMke0
A fair word of warning though - it is a lot less unbiased than this article, as I was trying to agitate the community to protest against the whole concept of chat reporting at the time.
These days exploits are much less numerous and severe, but they are nevertheless still present. I will go over them in detail below.
As was already briefly mentioned in the previous section, an adversary can leverage legitimate reasons for message exclusion to hide any messages from the report, their own or otherwise. More missing messages can potentially make the report more suspicious, but how exactly evidence of absence is acted upon on Mojang's end is unknown. This exploit is implemented by proof-of-concept Gaslight mod. It used to be a lot more powerful in 1.19.1/1.19.2, but its current functionality is still sufficient to hide messages from the report.
Since the system is very much dependent on players committing to seeing messages from other players, and the players can not commit for a variety of legitimate reasons, it is possible to simply not commit to seeing any messages at all. This particular exploit is implemented by Guardian, which is a client-side proof-of-concept mod that severely reduces the effectiveness of chat reports against its user. It does not provide complete immunity of course, but nevertheless makes sure that no context is ever included in the report against its user beyond user's own messages.
It is equally possible to commit to seeing messages at a later time, possibly impacting the context of the report. While this approach is specific and hard to exploit, it remains a security concern.
Example:
<PlayerA>: Are you good at pvp?
<PlayerB>: No | last_seen: []
<PlayerA>: Are you breaking the TOS?
<PlayerB>: Yes | last_seen: [ "Are you good at pvp?" ]
Should this conversation be reported, the report will picture the following:
<PlayerA>: Are you good at pvp?
<PlayerB>: Yes
Because of the way seen message commitment is implemented, it is impossible to have truly private message in this system. Information about private messages client have sent and received is leaked by its following public messages, since other clients can see that the some signatures in the list do not belong to any messages they themselves received, and therefore establish that that those messages must have been private.
This particular exploit is implemented by proof-of-concept Girlboss mod, which is a client-side mod that allows to detect when private messages are sent and/or received by other players. In 1.19.2 detection of private message sending was instantenous. Its efficiency was reduced with 1.19.3 patches, and now you have to wait until a player gives away their participation in a private conversation by sending a public message. It's no longer possible to establish who was the sender or receiver of the private message, only that two players were communicating privately.
For this particular exploit, an adversary has to collaborate with malicious server. They can leverage the fact that chat session updates undergo very little validation by other clients (as mentioned in Chat Sessions and Indexation section) to force other clients to reset their indexation of adversary's chat session. For example, the server can send a packet with null
chat session, followed by a packet establishing a "new" chat session with the same ID as the one that was just discarded. All clients will automatically accept first message in new chat session without checking its index. An adversary can leverage this behavior to create different messages with identical index, any one of which they can choose to include in the chat report, while excluding the others.
In the end, an adversary can produce a chat report with seemingly no gaps in indexation of their own messages, while in reality some messages were omitted. An important condition here is that both malicious server and all malicious clients collaborate to incriminate some particular victim. This exploit still awaits a proof-of-concept implementation.
These are all the exploits I am currently aware of; but beyond any doubt, they are not the only ones possible in this system. There are likely many more that have not been discovered yet, and some of them may be even more severe. I would like to encourage anyone who is interested in this topic to try to find more exploits, and to share their findings with the community.
While Mojang do not provide any "official" means of opting out of chat signing and reporting, they continue to maintain status quo with respect to community-built solutions for this purpose. The most popular one is No Chat Reports mod I develop myself, which is currently used by over 4 million players. It's available for both Fabric and Forge mod loaders, and is compatible with most popular modpacks.
Solutions in form of server plugins include NoEncryption, FreedomChat, No Chat Reports plugin (which is not developed by me), UnsignedMessages and others.
I hope this article has shed some light on the inner workings of chat reporting system, and how it can be exploited. I would like to thank Nodus team for their help in discussion of the exploits and development of some proof-of-concept mods mentioned in this article.
Mojang's communication on the matter of chat reporting has not been particularly impressive, but I realize that they are making an effort to improve the system. I hope that they will be able to do so in the future, and that this article can help them in their efforts, even though I do not personally support the concept of chat reporting in its current form.
Thanks