Cryptography - Ring Signatures

Mar 25, 2025·
Christopher Coverdale
Christopher Coverdale
· 5 min read

Ring Signatures

ring sigs

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:

PP: is the signer’s public key

xx: is the private key

HpHp: is the hash of P mapped to an elliptic curve point

II: 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 KK, 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 LL and RR:

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 LL and RR 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 LL, RR, and the message, propagating through the ring.

  1. 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 LL and RR 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:

  1. Recompute the challenge/response loop using the provided values.

  2. Confirm the loop wraps around correctly.

  3. 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 signed

  • pub_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 key
    • key_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]

Did you find this page helpful? Consider sharing it 🙌