This is the companion post to a tool, git-ring, that I recently released on Github.

Github/Gitlab makes the public keys of its users publicly available (at github.com/USERNAME.keys and gitlab.com/USERNAME.keys), this means that these services bind the users identity to public keys via this endpoint. Git-ring “exploits” this to enable the creation of (cryptographic) proofs showing membership among a set of users (or organizations) without revealing the identity of the person generating the proof. The public keys of everyone (including the signer) is automatically downloaded from Github, while the tool finds the correct private key on the signers local computer. All this combined makes using the tool pretty easy, as shown below, where we sign a message and then verify the resulting signature.

More examples can be found in the Github repository. The tool also supports manually supplying SSH public keys.

Git-ring is useful for things like whistleblowing: by signing a leak using git-ring an anonymous person proves that it originates from someone within the organization and hence the claims are likely authentic – without revealing their identity to anyone. The goal of this post is to give a quick behind-the-scenes look at how the (relatively simple) underlying cryptography works.

Ring Signatures

Git-ring uses ring signatures to implement the attestations described above. A ring signature scheme is like a regular digital signature scheme, except signing now additionally takes a set of public keys among which the real signer is hiding their identity i.e. the real signer is among the set of public keys, but the resulting signature does not reveal their identity (their public key). In other words the signing algorithm takes:

  1. A set of public keys pk1,,pkn \pk_1, \ldots, \pk_n of potential signers (called “the anonymity set” or “the ring”).
  2. The secret key sk \sk of the actual signer.
  3. The message msg \msg to sign.

And returns a digital signature, in other symbols:

σSign({pk1,,pkn},sk,msg) \sig \gets \sign(\{ \pk_1, \ldots, \pk_n \}, \sk, \msg)

Unlike a regular digital signature scheme, verification now takes a set of public keys (instead of a single key):

{0,1}Verify({pk1,,pkn},σ,msg) \bin \gets \verify(\{ \pk_1, \ldots, \pk_n \}, \sig, \msg)

For security, a ring signature scheme must satisfy anonymity and unforgability, which informally stated requires:

(Strong) Anonymity: σ\sig hides which of the signers pk1,,pkn \pk_1, \ldots, \pk_n created the signature (i.e. which pki \pk_i the sk \sk belongs to), even if the adversary has access to all the secret keys. This means privacy even in e.g. the scenario where an authority demands to get everyone’s secret keys (including the real signer’s) in an attempt to uncover the real signer.

(Strong) Unforgability: If the signer does not know the secret key for one of the public keys pk1,,pkn \pk_1, \ldots, \pk_n , he cannot produce a new valid signature σ \sig on any message msg \msg , i.e. produce σ\sig such that Verify({pk1,,pkn},σ,msg)=1 \verify(\{ \pk_1, \ldots, \pk_n \}, \sig, \msg) = 1 . This remains the case, even if the attacker is provided with previously valid signatures σ1,,σk \sig_1', \ldots, \sig_k' where i.σσi \forall i. \sig \neq \sig_i' .

While the original paper conceiving ring signatures by Rivest, Shamir and Tauman describes a construction based on RSA and only works for RSA keys. Git-ring instead use a generic technique by Cramer, Damgård and Schoenmakers based on Sigma protocols together with the Fiat-Shamir heuristic. This enables git-ring to include any mix of SSH keys (of different types) in the same ring, making it much more flexible.

To understand the construction we start by describing (loosely) what sigma protocols are.

two robots coversing

Intro to Σ\Sigma-Protocols

Σ\Sigma-protocols are especially simple/elegant classes of (public-coin interactive) zero-knowledge proofs-of-knowledge.

They have three rounds of communication:

  1. Prover sends a message (often called “the commitment”). Denoted “a”.
  2. The verifier sends a uniformly random challenge. Denoted “c”.
  3. The prover sends a response. Denoted “z”.

The verifier ends by checking the “validity” of the transcript (a,c,z) (a, c, z) against the statement (public key) and rejecting/accepting. The communication pattern is illustrated below (P for Prover and V for Verifier):

Protocol diagram of a sigma protocol
Sigma Protocol.: Consists of three rounds of communication

We require knowledge soundness, which informally states that a prover which does not know a witness (secret key) almost always fails to make the verifier accept. However for the purpose of this post, the most interesting feature of Σ\Sigma-protocols is that they are “special-honest verifier zero-knowledge”, meaning that there exists a simulator: an efficient algorithm which given the challenge cc produces complete transcripts (a,c,z)(a, c, z) which have the same distribution as the honest interaction. The only “special” power that the simulator has over the prover is the ability to sample a a after seeing c c , whereas the prover has to send a a to the verifier before receiving c c .

Σ\Sigma-Protocol for ECDSA and EdDSA Keys

Schnorr’s identification protocol is the quintessential example of Σ\Sigma-protocol. It proves knowledge of the discrete log in a cyclic group, i.e. that the prover knows xx st. pk=[x]G \pk = [x] \cdot G for group elements pk,G \pk, G where we use the additive notation for the group operation. It operates as follows:

  1. Prover samples r$ZG r \sample \ZZ_{|\GG|} , computes a=[r]G a = [r] \cdot G sends aG a \in \GG to the verifier.
  2. Verifier sends c$ZG c \sample \ZZ_{|\GG|} to the verifier.
  3. Prover responds with z=cx+rZG z = c \cdot x + r \in \ZZ_{|\GG|}

Finally the verifier checks:

[z]G=[c]pk+aG [z] \cdot G = [c] \cdot \mathsf{pk} + a \in \GG

Pictorially it looks as follows:

Protocol diagram of Schnorr's identification protocol
Schnorr's Identification Protocol.: Proves knowledge of the discrete log in a cyclic group.

Note the protocol above works for any cyclic group of known order, hence we can instantiate it with both edwards25519 or the NIST curves P-256/P-384/P-521 to show knowledge of an Ed25519 or ECDSA key respectively.

Soundness: To see why the protocol above is sound consider a prover which, after sending a a , could answers two different verifier challenges c1,c2 c_1, c_2 with z1,z2 z_1, z_2 such that both [z1]G=[c1]pk+a [z_1] \cdot G = [c_1] \cdot \pk + a and [z2]G=[c2]pk+a [z_2] \cdot G = [c_2] \cdot \pk + a , in other words: there exists at least two challenges for which the prover can convince the verifier. Then the secret key can be obtained as follows:

  1. Subtract [z2]G=[c2]pk+a [z_2] \cdot G = [c_2] \cdot \pk + a from [z1]G=[c1]pk+a [z_1] \cdot G = [c_1] \cdot \pk + a which eliminates a a :
[z1z2]G=[c1c2]pk [z_1 - z_2] \cdot G = [c_1 - c_2] \cdot \pk
  1. Divide by c1c2 c_1 - c_2 (which is non-zero since c1c2c_1 \neq c_2 ) on both sides:
pk=[z1z2c1c2]G \pk = \left[ \frac{ z_1 - z_2 }{ c_1 - c_2 } \right] \cdot G

Hence sk=(z1z2)(c1c2)1 \sk = (z_1 - z_2) \cdot (c_1 - c_2)^{-1}

The intuition for the section above is as follows: if the prover could answer two different challenges c1,c2 c_1, c_2 after sending a a , then they could also efficiently compute the secret key sk \sk (as shown above), hence a prover which does not know sk \sk can answer at most 1 challenge for each a a : therefore any cheating prover fails with probability 11G 1 - \frac{1}{|\GG|} .

Simulation: On the other hand, given c c it is easy to sample z,a z, a st. [z]G=[c]pk+aG [z] \cdot G = [c] \cdot \mathsf{pk} + a \in \GG without knowing sk \sk as follows:

  1. Pick z$ZG z \sample \ZZ_{|\GG|} at random.
  2. Solve [z]G=[c]pk+a [z] \cdot G = [c] \cdot \pk + a (the verifiers check) by setting a=[z]G[c]pkG a = [z] \cdot G - [c] \cdot \pk \in \GG

Σ\Sigma-Protocol for RSA Keys

Since RSA keys are still quite popular among SSH users we also need a way to prove knowledge of an RSA private key in such a way that we create a “cheating” proof if we know the challenge c c a head of time. The idea for this protocol is simple: the hard problem in RSA is to invert a permutation ψ:ZNZN \psi: \ZZ_{N}^* \to \ZZ_{N}^* (with ψ(x)=xemodN \psi(x) = x^e \mod N ) which can computed efficiently in the forward direction. Therefore to convince the verifier that the prover holds the secret key (and can therefore invert the permutation), we let the verifier sample a random element tZN t \in \ZZ_N for the prover to invert, the proof is the inverse z z , which the verifier can check by simply computing the permutation in the forward direction. i.e. check:

t=?ψ(z) t \overset?= \psi(z)

Since the verifiers challenge t t is chosen uniformly at random the protocol above convinces the verifier that the prover can invert the permutation with good probability, however it does not allow simulation – it’s not zero-knowledge.

Therefore instead of having the verifier send t t we form t=c+a t = c + a where the prover first chooses aZN a \in \ZZ_N and the verifier then samples cZN c \in \ZZ_N . This enables choosing t t arbitrarily by modifying a a if c c is know ahead of time, however if a a must be chosen first then the prover cannot control t t and its distribution is uniform over c c .

The protocol is verify simple:

  1. Prover samples random a$ZNa \sample \ZZ_N, sends a a to the verifier.
  2. Verifier sends c$ZN c \sample \ZZ_N to the prover, define t=c+aZN t = c + a \in \ZZ_N
  3. Prover sends z=ψ1(t)ZN z = \psi^{-1}(t) \in \ZZ_N

Verifier checks ψ(z)=tZN \psi(z) = t \in \ZZ_N . Pictorially:

Protocol diagram of the RSA sigma protocol
RSA Proof.:

Note tZN t \in \ZZ_N may not be in ZN \ZZ_N^* ; the range of ψ \psi , however in that case you can factor the modulus N=pq N = p \cdot q as p=gcd(t,N) p = \text{gcd}(t, N) , therefore this only occurs with negligible probability – so we can ignore this case. Likewise the case of t=0 t = 0 occurs with negligible probability.

Soundness: This protocol is not technically a Σ\Sigma-protocol: it does not allows extraction of the private key given two different responses to different challenges and the same message. However convincing the verifier does, trivially, reduce to inverting the RSA permutation: after sending a a , the verifier can make the prover invert any element t t by picking c=taZN c = t - a \in \ZZ_N , hence, if the prover succeeds with probability ϵ\epsilon he must be able to invert ψ() \psi(\cdot) on an ϵ\epsilon-fraction of ZN \ZZ_N .

Simulation: given c c , we can simulate an accepting transcript without inverting ψ \psi as follows:

  1. Pick z$ZN z \sample \ZZ_N .
  2. Compute tψ(z)ZN t \gets \psi(z) \in \ZZ_N
  3. Compute atcZN a \gets t - c \in \ZZ_N

This way a+c=tc+c=t=ψ(z) a + c = t - c + c = t = \psi(z) and the verifier accepts.


At this point we have interactive protocols enabling us to show that we know the private key corresponding to a particular SSH public key, but how do we show that we know a private key for one of many SSH public keys? Also how do we convert these interactive protocol into a signature?

Let’s deal with these two issues one at a time.

CDS Protocol Compiler: Proofs of Partial Knowledge.

Suppose you have a set of n n public keys pk1,,pkn \pk_1, \ldots, \pk_n and you want to prove that you know a secret key skα \sk_\alpha corresponding to one of the public keys pkα \pk_\alpha . Furthermore, you have (possibly distinct, possibly not) Σ \Sigma -protocols Π1,,Πn \Pi_1, \ldots, \Pi_n (for example the two described above) enabling you to prove knowledge of ski \sk_i for pki \pk_i using the protocol Πi \Pi_i . e.g. prove that you possess the Ed25519 \texttt{Ed25519} secret key of a Ed25519 \texttt{Ed25519} public key pki \pk_i .

a robot with question marks above its head

Obviously, the prover could just tell the verifier which index α \alpha he has the secret key for, then the prover and verifier run Πα \Pi_\alpha to prover/verify this fact. The obvious problem with the obvious protocol is that it leaks α \alpha : the verifier learns which public key the prover has a secret key for; this is bad, as it would allow anyone with the ring signature to trivially find out who signed the message :(

This problem is solved simply and elegantly by Cramer, Damgård and Schoenmakers. The idea is as follows:

  1. Since each Πi \Pi_i is a Σ \Sigma-protocol it has a simulator, this simulator outputs ai,zi a_i, z_i given the challenge ci c_i . The prover is going to use this to generate a first-round messages a1,,an a_1, \ldots, a_n for all the protocols Π1,,Πn \Pi_1, \ldots, \Pi_n as follows:
    • For each iα i \neq \alpha , the prover can pick ci c_i randomly, then run the simulator to obtain ai,zi a_i, z_i without knowing ski \sk_i .
    • For i=αi = \alpha , the prover generates the first message aα a_\alpha using he secret key skα \sk_\alpha he does know.
    • The prover sends a=(a1,,an) a = (a_1, \ldots, a_n) to the verifier.
  2. The verifier samples c c and sends it to the prover.
  3. The prover generates the response as follows.
    • Set the missing challenge cα c_\alpha to cα=c(iαci) c_\alpha = c \oplus \left(\bigoplus_{i \neq \alpha} c_i\right) (where \oplus is XOR)
    • Generates the missing last-round message zα z_\alpha with Πα \Pi_\alpha using the known secret key skα \sk_\alpha .
    • Sends z=(c1,,cn,z1,,zn) z = (c_1, \ldots, c_n, z_1, \ldots, z_n) to the verifier.

For each i i the verifier checks the validity of each transcript ai,ci,zi a_i, c_i, z_i as prescribed in Πi \Pi_i , and additionally that:

c=?ici c \overset?= \bigoplus_i c_i

If everything checks out, they consider the proof valid. Pictorially:

Protocol diagram of the CDS protocol compiler
CDS Protocol Compiler:: Converts a set of Σ \Sigma -protocols into a Σ \Sigma -protocol for proving knowledge of one of the secret keys.

I won’t formally prove that it is sound (it requires the definition of “Special-Soundness”, see the paper for details). The argument below does not quite hold all-the-way-home to a formal proof, but it provides some intuition for why it works:

  1. The prover gets to pick the challenges freely for all but one of the protocols.
  2. The simulated first round message ai a_i for iα i \neq \alpha , “fixes” the challenges ci c_i to which the prover can respond: after all: he did not know a secret key and could answer many different ci c_i after sending ai a_i – then he could cheat in the original protocol Πi \Pi_i .
  3. However, after fixing all ci c_i for iα i \neq \alpha the distribution of cα c_\alpha is uniform over the verifiers challenge c c .
  4. Therefore the probability of the prover cheating in Πα \Pi_\alpha remains the same as before applying the compiler.

Overall, he gets to “cheat” (read simulate) in all-but-one of the executions Π1,,Πn \Pi_1, \ldots, \Pi_n and the verifier never learns which: from the verifiers point of view they just see n n random strings c1,,cn c_1, \ldots, c_n subject to c=ici c = \bigoplus_i c_i and n n valid transcripts (a1,c1,z1), (a_1, c_1, z_1), , \ldots, (an,cn,zn) (a_n, c_n, z_n) .

It is elegant (e.g. it requires no additional cryptography) and works for any set of Σ \Sigma -Protocols. The only potential draw-back of the compiler above is that the proof is now n n times larger. Some people found ways to optimize that for a broad class of protocols.

The next challenge is to get rid of interaction.

Fiat-Shamir Heuristic: From Interactive Protocol to Signatures.

So now we have an interactive proof that the prover knows one of the public keys. But a (ring) signature is non-interactive: you do not need to connect to anyone over the internet to verify the validity of a signature σ \sig .

a robot reading a letter

Luckily, there is a simple transformation (Fiat-Shamir) which converts 3-round “public-coin protocols” (protocols in which the verifier “has no secrets”) into non-interactive arguments: roughly speaking the idea is to replace the verifiers challenge c c with a hash of the first round message a a . Let H \hash be a cryptographic hash function (e.g. SHA512), instead of communicating with the verifier the prover computes a proof like this:

  1. Compute the first-round message a a from skα \sk_\alpha according to Π \Pi
  2. Generate the verifiers challenge on your own as: cH((pk1,,pkn)  a) c \gets \hash\left(\left(\pk_1, \ldots, \pk_n\right) \ \Vert \ a\right)
  3. Compute z z according to Π \Pi from skα,c \sk_\alpha, c .

The proof is σ=(a,z) \sig = (a, z) , the verifier recomputes the challenge c c from a a and checks the validity of the transcript according to Π \Pi . Pictorially:

Converts a Σ \Sigma -protocol into a signature scheme.
Fiath-Shamir Heuristic:: Converts a Σ \Sigma -protocol into a signature scheme.

If you want a more in-depth description consider reading the ZKDocs on Fiat-Shamir. Technically, to prove security, this cryptographic hash function must take the form of a “random oracle” – which provable cannot be instantiate by any hash function. Nonetheless this transformation seems to work out fine in practice. See What is the Random Oracle Model and why should you care? by Matthew Green if you are interested in this strange beast.

Before we continue let us briefly cover a few subtleties when applying Fiat-Shamir:

  • It is important to include the list of public keys (“the statement”) in the hash, otherwise a malicious prover can generate proofs for rings in which he does not know any private key.
  • In the description above we simply concatenate the fields before hashing, however it is important to separate the fields (e.g. by including the length as done in git-ring)

So we now have a proof which is non-interactive, since the verifier can simply recompute c c and check the validity of the transcript, however it is not bound to a message… it is not a signature. Luckily it is very easy to obtain a signature scheme: you simply add the message msg \msg you want to sign when computing the hash, the signer generates the signature as follows:

  1. Compute the first-round message a a from skα \sk_\alpha according to Π \Pi
  2. Generate the verifiers challenge on your own as: cH(msg  (pk1,,pkn)  a) c \gets \hash\left(\msg \ \Vert \ \left(\pk_1, \ldots, \pk_n\right) \ \Vert \ a \right)
  3. Compute z z according to Π \Pi

The signature is the transcript, i.e. a,z a, z . The verifier recomputes c c using H() \hash(\cdot) from pk1,,pkn \pk_1, \ldots, \pk_n , a a and msg \msg , then checks that a,c,z a, c, z is an accepting transcript according to the definition of Π \Pi : running an instance of the protocol Π \Pi in which the challenge c c and provers messages a,z a, z are hard-coded.

Wrap-Up

We constructed heterogeneous ring-signatures by:

  • First describing a couple of Σ\Sigma-protocols (one for each key-type).
  • Then applied the CDS compiler to get a protocol enabling the prover to show that they know one-of the secret keys.
  • Finally we used the Fiat-Shamir heuristic to convert that protocol into a signature scheme.

That’s the end of the road. If you are interested in how the implementation works take a look at the code.