cstef
/posts/shamir
TOP

Shamir Secret Sharing in a Nutshell

How to split a secret among multiple persons, so that only any subset of n people can recover it ?

04/12/2024 · 15 min read · sapling 🪴

Info

I am not a cryptographer, nor a mathematician. This article is the result of my own research and understanding of the subject. If you find any mistakes, please let me know!

The vast majority of what is written here is taken from various sources, which are listed at the end of this article. I highly recommend you to read them if you want to dive deeper into the subject.

The main idea behind Shamir Secret Sharing (SSS) is to split a secret ss into nn parts, such that any kk parts can be used to reconstruct the secret, but any k-1k-1 parts are not enough to do so, and do not give any information about the secret.

Splitting the Secret

Let’s take the following example: we want to split the secret s = 42s = 42 into n = 5n = 5 parts, such that any k = 3k = 3 parts can be used to reconstruct the secret. It is supposed we are working in a finite field FF_qFF_q for the entirety of this post.

What the hell is FF_qFF_q?

A finite field FF_qFF_q where q = p^k | p in cal(P)q = p^k | p in cal(P) (qq is a prime power), is a finite set of elements, on which we can apply our usual additions and multiplications.

The most common finite field is the set of integers modulo qq, ZZ/q ZZ = ZZ_qZZ/q ZZ = ZZ_q, where all computations are taken mod qmod q, which means that we have:

ZZ_q = {0,1,2,..., q-1}ZZ_q = {0,1,2,..., q-1}

In the case of an elliptic curve, qq is our curve’s order.

The first step is to sample a random polynomial f(x) = P_(k-1) (x) = a_0 + a_1 x + ... + a_(k-1) xf(x) = P_(k-1) (x) = a_0 + a_1 x + ... + a_(k-1) x of degree k-1k-1 such that a_0 = sa_0 = s. This gives us the property f(0) = sf(0) = s.

f(x) = P_2 (x) = 42 + 5 x + 3 x^2f(x) = P_2 (x) = 42 + 5 x + 3 x^2

Our splits, also called shares, are in fact just points of our polynomial. We can generate them by evaluating f(x)f(x) for x in S = {1,2,3,4,5}x in S = {1,2,3,4,5}. Do not evaluate for x = 0x = 0 as this would obviously just give away the secret.

z_1 &= f(1) = 42 + 5 + 3    &=& 50 \
z_2 &= f(2) =z_1 &= f(1) = 42 + 5 + 3    &=& 50 \
z_2 &= f(2) =

Thus, our shares are Z_1(1, 50)Z_1(1, 50), Z_2(2, 64)Z_2(2, 64), Z_3(3, 84)Z_3(3, 84), Z_4(4, 110)Z_4(4, 110) and Z_5(5, 142)Z_5(5, 142).

Let’s now plot the polynomial f(x)f(x):

#set text(size: 10pt)
#let f(x) = 42 + 5 * x + 3 *#set text(size: 10pt)
#let f(x) = 42 + 5 * x + 3 *

Reconstructing the Secret

We know that n+1n+1 points Z_i (x_i, y_i) | i in SZ_i (x_i, y_i) | i in S will suffice to construct the polynonial P_n(x)P_n(x) of degree nn.

In our case, deg(f(x)) = k-1deg(f(x)) = k-1, so we need (k-1)+1 = k(k-1)+1 = k points to restore f(x)f(x), just as described in the beginning.

Based on the shares we generated earlier, let’s take Z_1(1, 50)Z_1(1, 50), Z_3(3, 84)Z_3(3, 84) and Z_5(5, 142)Z_5(5, 142) (our recovery group R = {1,2,5}R = {1,2,5}) to reconstruct the polynomial f(x)f(x) using Lagrange interpolation:

Tip

If you’re curious about Lagrange interpolation, I have written a quick explaination for you to read.

f(x) = sum_(i in R) y_i dot overbrace(product_(j if(x) = sum_(i in R) y_i dot overbrace(product_(j if(x) &= 50 dot ((x-3)(x-5))/((1-3)(1-5)) + 84 dot f(x) &= 50 dot ((x-3)(x-5))/((1-3)(1-5)) + 84 dot

It’s even cooler when represented graphically:

#set text(size: 10pt)

#let l1(x) = ((x - 3) * (x #set text(size: 10pt)

#let l1(x) = ((x - 3) * (x

Commitments, Proofs and Verifications

In a perfect world where everyone is honest and where there are no transmission errors caused by cosmic rays, we could just send the shares to the participants and call it a day. But guess what? Sh*t happens.

We need a way to check that the share we receive as a shareholder after the secret has been split is actually a valid one. You could gather with other bearers and collectively verify if the recovered secret is correct, but that is just too much hassle for such a simple task.

Instead, let’s take advantage of the properties of elliptic curves to create a commitment scheme. After we have generated our polynomial f(x)f(x), we can take each coefficient a_ia_i and multiply it by the generator point GG of the curve. This gives us a few values C = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | pC = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | p that we can send to the shareholders.

Tip

If you are not familiar with basic elliptic curve stuff, I recommend that you read my post on the subject or the references listed at the end of this article.

When a shareholder wants to verify their share Z_i = (i, f(i))Z_i = (i, f(i)), they can check with the following equation:

f(i) dot G &= sum_(j=0)^(k-1) (phi.alt_j dot i^j) f(i) dot G &= sum_(j=0)^(k-1) (phi.alt_j dot i^j)

You could see this procedure as computing the “public keys” of the coefficients. This method is also called “Feldman’s Verifiable Secret Sharing”.

One may argue that disclosing a_0 dot G = s dot Ga_0 dot G = s dot G could give information about the polynomial, but if we suppose that ss is an EC secret key, the public key s dot Gs dot G is supposed public and may be shared. Finding ss with s dot Gs dot G comes down to solving the discrete logarithm problem, which is supposed really hard here.

Another way the dealer could commit to the polynomial he generated without directly sharing s dot Gs dot G, is to add a so-called “blinding polynomial”, a pretty common concept in cryptography.

Let’s now instead take phi.alt_i = a_i dot G + b_i dot Hphi.alt_i = a_i dot G + b_i dot H where b_ib_i comes from a randomly generated polynomial g(x) = b_0 + b_1 x + ... + b_(k-1) x^(k-1)g(x) = b_0 + b_1 x + ... + b_(k-1) x^(k-1), our blinding polynomial. H != GH != G is just another generator point on the curve. The dealer will now needs to distribute slightly different shares Z_i = (i, f(i), g(i))Z_i = (i, f(i), g(i)).

Shareholders may now verify their shares with:

f(i) dot G + g(i) dot H &= sum_(j=0)^(k-1) (phi.alf(i) dot G + g(i) dot H &= sum_(j=0)^(k-1) (phi.al

This second method is known as “Pedersen’s Verifiable Secret Sharing”.

Bob just got hit by a bus, what now?

Our good old friend disappeared along with his share, and now other bearers are scared of losing too many shares until they can’t recover the secret. They could reiterate the dealing procedure by all sending their shares to a single person which then redistributes the new ones. However this is not feasible in the case where everyone distrusts each other. We need a multi-computational way of re-issuing a new share without someone ever recovering the secret.

We will need kk shareholders, denoted RR to re-issue a new share Z_ell = (ell, f(ell)) = (ell, z_ell)Z_ell = (ell, f(ell)) = (ell, z_ell).

Each shareholder begins by computing their Lagrange multiplier:

l_i (ell) = product_(j in R,j!=i)(ell-j)/(i-j)l_i (ell) = product_(j in R,j!=i)(ell-j)/(i-j)

After multiplying with their share z_i = f(i)z_i = f(i), they randomly split it into kk so-called Lagrange-parts partial_(i,j)partial_(i,j) in order to distribute them to other bearers jj:

z_i dot l_i &= partial_(i,1) + partial_(i,2) + ...z_i dot l_i &= partial_(i,1) + partial_(i,2) + ...

The exchange matrix EE can be represented as:

E_(k times k) = mat(
    partial_(1,1), partial_(1E_(k times k) = mat(
    partial_(1,1), partial_(1

Where the iith row corresponds to the Lagrange-parts that the shareholder ii will send and the jjth column to the Lagrange-parts the shareholder jj will receive.

Each shareholder jj computes the partial-share:

sigma_j = sum_(i in R) partial_(i,j)sigma_j = sum_(i in R) partial_(i,j)

Where partial_(i,j)partial_(i,j) is the jjth Lagrange-part of ii. They respectively send sigma_jsigma_j to the new bearer ellell, which finally computes his share:

z_ell &= sum_(i in R) sigma_j \z_ell &= sum_(i in R) sigma_j \

This can be rewritten as:

sum_(j in R) sigma_j &= sum_(j in R) sum_(i in R) sum_(j in R) sigma_j &= sum_(j in R) sum_(i in R)

Inception

Another angle to tackle this problem from is to re-use Secret Sharing inside our Secret Sharing scheme (Inception, anyone?).

Let’s say we have our recovery group RR, our new shareholders NN and A = R union NA = R union N for convenience.

  1. Each shareholder i in Ri in R generates a random polynomial g_i (x)g_i (x) of degree k-1k-1.
  2. They each compute the auxiliary shares d_(i,j) = g_i (j)d_(i,j) = g_i (j) for j in Aj in A.
  3. Every shareholder j in Aj in A receives the auxiliary shares d_(i,j)d_(i,j) from the recovery group.
  4. Each shareholder j in Rj in R computes the aggregated share H(j) = u_j = z_j + sum_(i in R) d_(i,j)H(j) = u_j = z_j + sum_(i in R) d_(i,j) and shares it to everyone in NN.
  5. Each future bearer j in Nj in N can interpolate the polynomial H(x)H(x) from the shares u_ju_j and compute their share z_j = H(j) - sum_(i in R) d_(i,j) = u_j - sum_(i iz_j = H(j) - sum_(i in R) d_(i,j) = u_j - sum_(i i

Let’s take a look at the math:

z_ell &= (H(ell) - sum_(i in R) d_(i, ell)) | ell z_ell &= (H(ell) - sum_(i in R) d_(i, ell)) | ell

But wait! We just learnt how to implement a secure, verifiable secret sharing scheme and we aren’t even using it!

Verifiable Inception

Let’s review the protocol, this time including relevant checks to ensure the data we’re receiving is genuine. We assume the original dealer was a good guy and already used Feldman’s VSS, providing the commitments C = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | pC = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | p for the OG polynomial f(x) = a_0 + a_1 x + ... + a_(k-1) x^(k-1)f(x) = a_0 + a_1 x + ... + a_(k-1) x^(k-1) to everyone.

  1. Each shareholder i in Ri in R generates a random polynomial g_i (x) = b_(i,0) + b_(i,1) x + ... + b_(i,k-1) x^g_i (x) = b_(i,0) + b_(i,1) x + ... + b_(i,k-1) x^ of degree k-1k-1.
  2. They also compute and share for j in Rj in R to see their commitments: Gamma_i = {psi_(i,0), psi_(i, 1), ..., psi_(i, k-1Gamma_i = {psi_(i,0), psi_(i, 1), ..., psi_(i, k-1
  3. They each compute the auxiliary shares d_(i,j) = g_i (j)d_(i,j) = g_i (j) for j in Aj in A, and shares them once they have received all other Gamma_i | i in RGamma_i | i in R
  4. Every shareholder j in Aj in A receives the auxiliary shares d_(i,j)d_(i,j) from i in Ri in R and checks whether they match Gamma_iGamma_i d_(i,j) dot G &= sum_(m=0)^(k-1) psi_(i,m) dot j^md_(i,j) dot G &= sum_(m=0)^(k-1) psi_(i,m) dot j^m
  5. Before computing anything else, each shareholder j in Rj in R commits to the upcoming H(x)H(x) polynomial and sends it to i in Ai in A: T = {theta_i}_(i in R) | theta_i = sum_(k in R) psT = {theta_i}_(i in R) | theta_i = sum_(k in R) ps
  6. Each shareholder j in Rj in R computes the aggregated share H(j) = u_j = z_j + sum_(i in R) d_(i,j)H(j) = u_j = z_j + sum_(i in R) d_(i,j).
  7. Each inductee i in Ni in N first waits to receive all TT, checks that they are the same from everyone, and then receives u_j | j in Ru_j | j in R.
  8. Each future shareholder i in Ni in N checks whether each given u_j | j in Ru_j | j in R is genuine: u_j dot G &= sum_(i=0)^(k-1) phi.alt_i + sum_(i inu_j dot G &= sum_(i=0)^(k-1) phi.alt_i + sum_(i in
  9. Each future bearer j in Nj in N can interpolate the polynomial H(x)H(x) from the shares u_ju_j and compute their share z_j = H(j) - sum_(i in R) d_(i,j) = u_j - sum_(i iz_j = H(j) - sum_(i in R) d_(i,j) = u_j - sum_(i i

Single share issuing

In the case where we only want to issue a single share, conduition proposed a clever way to remove the subtracting step at the end by adding a root at ell | {ell} = Nell | {ell} = N to the blinding polynomial. This way, we have:

g_i (x) = (x - ell) dot P_(k-2) (x) | i in S, spacg_i (x) = (x - ell) dot P_(k-2) (x) | i in S, spac

When interpolating H(ell)H(ell), the blinding polynomials g_i (ell) | i in Sg_i (ell) | i in S cancel out and we directly get z_ellz_ell:

z_ell &= H(ell) \
      &= sum_(i in S) (z_i + u_iz_ell &= H(ell) \
      &= sum_(i in S) (z_i + u_i

Note that we could try to add multiple roots: (x- ell_1)(x-ell_2) dot g_i (x)(x- ell_1)(x-ell_2) dot g_i (x), but then all the new shareholders would have access to the new shares.

You could also apply the same verifying scheme as before to this method.

We have a problem

When recovering the secret, we need to trust a single recoverer to do so and all send him all of our shares. But what if we all wanted to recover the secret at the same time?

One may naively try to implement a simple broadcast or pairwise exchange protocol, but if a single malicious actor P_mP_m wanted to prevent the others from also recovering the secret, he could just wait for everyone to send their share, and never send his. He will have all the shares needed, while the others can’t do anything.

If everyone distrusts each other, we will have this strange situation where no one wants to send their share first. One may prove that they own a valid share with a Zero-Knowledge Proof ZKP, but will never be able to prove that he’s actually going to send it to the others.

One “solution” to this problem (in my opinion the easiest to implement) is to set a pre-defined order in which shareholders need to reveal their shares, after which it is verified each time by the others. If the share is invalid or the bearer fails to send it within a given time, the others can post a complaint against this user and abort the process.

Commitments-based ordering

After each shareholder has shared his commitment C_i = y_i dot GC_i = y_i dot G and it has been verified by everyone else, the order of reveal is defined by the value of each commitment. Let’s recall our commitments for f(x) = a_0 + a_1 x + ... + a_(k-1) x^(k-1)f(x) = a_0 + a_1 x + ... + a_(k-1) x^(k-1):

C = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | pC = {phi.alt_0, phi.alt_1, ..., phi.alt_(k-1)} | p

One cannot manipulate the value of y_i dot Gy_i dot G as it is verifiable by the others with

y_i dot G = sum_(i = 0)^(k-1) phi.alt_iy_i dot G = sum_(i = 0)^(k-1) phi.alt_i

The probability of a malicious actor being placed last in the queue and thus fool everyone is 1/k1/k for kk recoverers if we suppose that y_i dot Gy_i dot G is pseudorandom. Not that good.

Incremental recovery

Let’s instead require each bearer to first send their share to everyone placed before them in the pre-defined order.

#import "@preview/fletcher:0.5.2" as fletcher: *

#import "@preview/fletcher:0.5.2" as fletcher: *

We have a single shared state RR that needs to be kept up-to-date for everyone at each round. This is to keep track of who has and who hasn’t sent their share yet. The state SS consisting of every sent share does not need to be shared as everyone can keep track of the broadcasts to RR received.

This way, a single malicious shareholder cannot dupe the rest of the group. But what if two malicious actors P_mP_m and P_m'P_m' tried hijacking the recovery ?

  1. P_1P_1 initiates S = {y_1}, space R = {P_1}S = {y_1}, space R = {P_1}
  2. P_2P_2 sends y_2y_2 to RR, and gets accepted: S = {y_1, y_2}, space R = {P_1}S = {y_1, y_2}, space R = {P_1}
  3. P_mP_m joins normally as well, he gets access to SS
  4. P_3P_3 joins, and so on for other P_iP_i
  5. P_m'P_m' only sends his share to P_mP_m, which sends him SS back (or not)

In this case, P_mP_m (and P_m'P_m') are the only ones able to recover the secret. This isn’t of any good either…

TODO: find out if this is actually feasible?

References / Suggested readings