Cryptography - Ring Signatures


Ring Signatures

Why Ring Signatures?
Ring Signatures are a cryptographic technique that enable a signer to remain anonyomous within a group of public keys. This allows the signer to prove someone in the group signed a message and that they are one of them but doesn’t reveal which key signed.
Overview
Below are some important properties:
Anonymity: the identity of the actual signer is concealed among a set of possible signers
Unlinkability: if the same signer signs multiple messages, those signatures cannot be linked back to the same individual
Verifiability: the signature can be verified without needing to identify the real signer and also with the property of being unable to identify the signer
What are Ring Signatures?
Ring signatures consist of several key pieces:
An array of public keys (The ring)
This is the anonymity set. The signer selects a group of public keys — some are decoys, and one is their actual public key. This allows them to hide in plain sight.
Only one of the keys in the ring belongs to the actual signer. The cryptographic trick is to construct a valid signature without revealing which one.
An array of responses and challenges
Used with the challenges to show knowledge of the private key without revealing it.
This is used in a zero-knowledge proof loop. The signature must “wrap around” the ring — making it internally consistent — while proving knowledge of the private key without revealing it.
A message to sign
This is the content being signed. Including it binds the signature to the message, ensuring integrity and preventing replay attacks.
How Ring Signatures are constructed
Select the Ring
The signer collects a set of public keys. This set includes decoys and the signer’s real public key, randomly placed in the ring. The signer notes their own index.
Generate the Key Image
The key image is a critical concept. It ensures that even though the signer is anonymous, double-signing can still be detected.
Hp = hash_to_point(P)
I = x * Hp
Where:
: is the signer’s public key
: is the private key
: is the hash of P mapped to an elliptic curve point
: is the key image
In Monero, each transaction output has a unique one-time public key. A key image ensures that each output can only be spent once — if the same image appears again, it’s flagged as a double spend.
Begin the Signature Loop: Generate L and R
Next, the signer picks a random secret scalar , and uses it to compute the values:
# Compute key image.
P = pub_keys[signer_index].pubkey.point
x = priv_key.privkey.secret_multiplier
Hp = hash_point_to_point(P)
key_image = x * Hp
# Step 1: choose random K and compute first commitment
K = secrets.randbelow(order)
L = K * G
R = K * Hp
These values serve as the starting point for the challenge-response ring. The first challenge is computed by hashing the message and the values of and :
data = message + L.x().to_bytes(32, "big") + R.x().to_bytes(32, "big")
challenge[(signer_index + 1) % n] = H(data)
Loop Through the Ring
Now, the signer begins generating fake responses (for decoy public keys) and computes corresponding values of and using these fake responses and the previous challenge:
i = (signer_index + 1) % n
for _ in range(n - 1):
# Generate fake responses for the decoy pubkeys.
response[i] = secrets.randbelow(order)
P_i = pub_keys[i].pubkey.point
Hp_i = hash_point_to_point(P_i)
# Schnorr-style commitment for public key.
L = response[i] * G + challenge[i] * P_i
# Commitment for hashed public key and key image.
R = response[i] * Hp_i + challenge[i] * key_image
# Add the challenge to the ring, it will be used in the next
# calculation
data = message + L.x().to_bytes(32, "big") + R.x().to_bytes(32, "big")
challenge[(i + 1) % n] = H(data)
i = (i + 1) % n
Each time, a new challenge is generated based on , , and the message, propagating through the ring.
- Close the Loop with the Real Response
Once the challenge loop comes back around to the signer’s original index, they compute the real response value s[signer_index] such that the final and match and close the ring — making the signature verifiable without revealing which index they signed from.
Verifying Ring Signatures
To verify the signature, anyone can:
Recompute the challenge/response loop using the provided values.
Confirm the loop wraps around correctly.
Check that the key image hasn’t appeared before (to prevent double-spends).
The required variables for the receiver are:
message
: The message that was signedpub_keys
: The array of public keys (i.e., the ring)signature
: A tuple containing:init_challenge
: The initial challenge (starting point of the ring)response
: An array of response scalars, one per public keykey_image
: The unique point derived from the real signer’s private key
def ring_verify(message: bytes, pub_keys, signature):
init_challenge, response, key_image = signature
n = len(pub_keys)
challenge = [0] * (n + 1)
challenge[0] = init_challenge
for i in range(n):
P_i = pub_keys[i].pubkey.point
Hp_i = hash_point_to_point(P_i)
L = response[i] * G + challenge[i] * P_i
R = response[i] * Hp_i + challenge[i] * key_image
data = message + L.x().to_bytes(32, "big") + R.x().to_bytes(32, "big")
# Compute and assign the next challenge.
challenge[i + 1] = H(data)
# The final computed challenge must loop back to the init_challenge.
return challenge[0] == challenge[n]