Signal has rolled out usernames, meaning users can now use the app while keeping their phone numbers private. This enhanced level of privacy was achieved through the use of Ristretto hashes and zero-knowledge proofs.
We wanted to take a deeper look into how these two cryptographic primitives can provide another privacy protection for Signal’s users; fortunately, the source code is open-source, so let’s dive in.
This blog post was adapted from the original Twitter thread, which can be found here↗.
What is a Ristretto Hash?
Ristretto is an elliptic-curve group (related to but not the same as Curve25519), and the corresponding hash is a hash-to-curve algorithm, meaning it is a hash algorithm that takes in a scalar input and outputs a curve point on the Ristretto group.
Looking at the Source
The source code is public on signal’s github here↗ so we can freely take a look in order to understand how they implemented the username functionality.
We can see the Username
struct contains a nickname that is a string, an integer discriminator
, and some mystery scalars.
pub struct Username {
nickname: String,
discriminator: u64,
scalars: Vec<Scalar>,
}
Looking at the deserialization code, we can infer the discriminator is simply the two digits that valid signal usernames are required to contain.
pub fn new(s: &str) -> Result<Self, UsernameError> {
let (nickname, discriminator) =
s.rsplit_once('.').ok_or(UsernameError::MissingSeparator)?;
Self::from_parts_without_soft_limit(nickname, discriminator)
}
What are the Scalars?
The scalars contain the following three values (some encoding details have been omitted for simplicity):
sha512 (nickname, discriminator)
nickname
(compressed)discriminator
How does the Hash Work?
Now that we understand the contents of the Username
struct, the hash is pretty simple. The three scalars all belong to the curve’s scalar field (i.e., can be multiplied by the points on the curve). The hash algorithm has three constant generator points on the curve as its parameters. Let’s call them , , and . The hash is then simply calculated as
Hiding Your Phone Number
Once a user generates their username, they can compute the hash and send it to the server for storage. Note that since only the hash is sent to the server, the server can never infer the username associated with the phone number.
The user should thereafter be able to restrict who can message them. For example, in order to invite Bob to message her, Alice provides him with the plaintext of her username (nickname + discriminant).
Putting it Together with ZK
In order to connect with Alice, Bob must prove they know Alice’s username. In other words, Bob must prove they know the preimage of the username hash stored with the server without revealing the plaintext. To do this, Bob generates a ZK proof for the following statement:
I know a nickname, discriminant, and some value H, such that username_hash == H*G_1 + nickname*G_2 + discriminator*G_3
. After Bob proves he knows the preimage of the username hash, he is then able to successfully message Alice.
The ZK Scheme
The statement Bob wishes to prove is knowledge of hash preimage. Since the hash is based on elliptic-curve scalar multiplication, an efficient way to do this is to adapt schemes that prove knowledge of discrete log. The exact scheme used here is a variant of the sigma protocol↗ that adapts it to work for multiple bases.
About Us
Zellic specializes in securing emerging technologies. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants.
Developers, founders, and investors trust our security assessments to ship quickly, confidently, and without critical vulnerabilities. With our background in real-world offensive security research, we find what others miss.
Contact us↗ for an audit that’s better than the rest. Real audits, not rubber stamps.