Shamir Secret Sharing¶

References SSS:

  • [Shamir] How to share a Secret, Adi Shamir. 1979
    • Shamir's original paper
  • [SSS] Wikipedia page on Shamir secret sharing
  • [FFA] Wikipedia page on finite field arithmetic

References SSS in Cryptocurrencies:

  • SLIP39 specification
  • BC-SSKR
  • Shamir39

Sage Dependencies:

This notebook uses sage math as kernel

In [1]:
import sage.misc.banner # sage math version info 
rchars = "┘─│┐┌└"
smallbanner = sage.misc.banner.banner_text(full=True)
for c in rchars:
    smallbanner = smallbanner.replace(c,"") # remove ascii art box
print(smallbanner)
#banner() # full banner with box
 SageMath version 9.5, Release Date: 2022-01-30                     
 Using Python 3.10.12. Type "help()" for help.                      

Python Dependencies:

In [2]:
from typing import List, Optional, Sequence, Tuple
import secrets
# import from custom path:
import sys
IMPORT_PATH='./src/sss'
if IMPORT_PATH not in sys.path:
    sys.path.append(IMPORT_PATH)
#print(sys.path)
import sss     # Plain SSS
import sssgf   # SSS over GF(256)
import ssswiki # SSS as implemented on Wikipedia, with pseudo-random feature
# reload imports on cell-rerun in case of source code modifications:
import importlib
importlib.reload(sss)
importlib.reload(sssgf)
importlib.reload(ssswiki)
Out[2]:
<module 'ssswiki' from '/workdir/./src/sss/ssswiki.py'>

Methods of secret sharing the secret $ s $¶

The basic idea of Shamir secret sharing (SSS) is it, to distribute $ n $ shares of a secret value $ s $, s.t., a threshold of any $ t $ shares can be used to reconstruct the original secret $ s $. Hereby, $ t $ can be defined to take any value between $ 1 $ and $ n $ s.t., $ 1 \leq t \leq n $. Any number of shares below the threshold $ t $ provides no information about the shared secret $ s $ making the scheme infomration theoretically secure.

We will describe two basic methods of SSS:

  1. Sharing the secret $ s $ at once using some finite field, where the size of the finite field must be larger than the secret to be shared.
  2. Sharing the secret $ s $ byte-per-byte, which requires splitting the secret $ s $ into smaller chunks (one byte each) and sharing each of the chunks useing some small finite field.
In [3]:
b = 16 # number of bytes of the secret to be shared
s = secrets.token_bytes(b) # randomly selecting a secret 
print(f"s in hex     = {s.hex()}")
s in hex     = 9fd47c7bd94aeca621715e359135657c

Sharing secret $ s $ at once over $\mathbb{F}_p$¶

In the following, we specify the process of creating $1 \leq n $ shares $(s_1, s_2, ..., s_n)$ for a given secret $s$ such that any subset of at least $1 \leq t \leq n$ of those shares can be used to recover the shared value. In this case we chose a prime field $\mathbb{F}_p$, s.t. the order of the field $ q=p $ and $ p \in \text{Primes}$.

We want to share a secret $ s $, which in our example is a sequence of $b \geq 16$ bytes. To to that, we require a prime $ p > s $ to define the prime field $\mathbb{F}_p$.

Sharing over $\mathbb{F}_p$¶

This describes classical SSS, where we share the secret $ s $, at once, using a finite field $\mathbb{F}_p$, where $ p \in \text{Primes}$.

Input:

  • $\mathbf{s}$: The secret to be shared, i.e., a sequence of $b$ bytes, where $b \geq 16$. This is the only parameter that must be kept secret.
  • $\mathbf{n}$: The number of shares to be generated, $1\leq n < |\mathbb{F}_p|$, (i.e., $ n < p $).
  • $\mathbf{p}$: The prime $ p $ defining the prime field $\mathbb{F}_p$.
  • $\mathbf{t}$: The secret sharing threshold, $1 \leq t \leq n$. This value specifies the minimum number of shares required for the recovery process.

Output:

  • $\mathbf{s_1, s_2, \dots, s_n}$: A list of $n$ shares. Each share $s_i$ is represented as a tuple $(x_i, y_i)$ consisting of the share index $x_i = i$ (an integer) and the share value $y_i$ (a sequence of $b$ bytes).

Construct the secret sharing polynomial:

The first step is it to construct the secret sharing polynomial $f(\cdot)$ of degree $t - 1$. In this polynomail the coefficient $c_0$ is set to be the secret that should be shared, i.e., $c_0 = s$. The other $t-2$ coefficients (of the same size) have to be selected uniformly at random. $$ f(x) = c_0 + c_1 \cdot x + c_2 \cdot x^2 + \dots + c_{t - 1} \cdot x^{t - 1} \pmod p $$ Here the values of $c_0, c_1, \dots, c_{t - 1}$ are used to represent coefficients. The list of coefficients $c_j \mid 0 \leq j < t$ is defined as follows: $$ c_j = \begin{cases} s &\text{if } j = 0 \\ \textsf{random}(b) &\text{if } 0 < j < t \\ \end{cases} $$

Evaluate the secret sharing polynomial:

In the next step, the constructed polynomial is evaluated over $\mathbb{F}_p$. Therefore, all computations have to be performed modulo $p$. For additional background information regarding finite field arithmetic we refer the reader to the Wikipedia page on Finite Field Arithmetic.

Given this definition of coefficients (and thus of the secret sharing polynomials), the $n$ shares $s_1, s_2, ..., s_n$ are computed as follows: $$ s_i = \Big( i, f(i) \Big) \quad 1 \leq i \leq n $$ Note that a share is a tuple of two values and MUST include the share index $i$ as well as the share value, obtained by evaluating the secret sharing polynomials and concatenating the results.

def share(s: bytes, n: int, t: int, prime: int, pseudo_random_seed: bytes=None) -> List[Tuple[int, bytes]]:
    """ Main function for creating `n` shares of a secret `s` such that any subset of at least `t` generated shares can be used to recovered the secret `s`.
    """
    b = len(s)
    if not (1 <= t <= n):
        raise ValueError("Invalid secret sharing parameters, ensure that 1 <= t <= n holds.")
    if b*8 > prime.bit_length():
        raise ValueError("Secret too large for chosen finite field prime.")
    if n > prime:
        raise ValueError("Number of shares to large for chosen finite field prime.")

    # Generate t coefficients
    c: List[bytes] = [b""] * t
    for j in range(t):
        if j == 0:
            c[j] = s
        else:
            if pseudo_random_seed:
                c[j] = get_pseudorandom(b,pseudo_random_seed)
            else:
                c[j] = get_random(b)

    # Evaluate polynomial and generate shares 
    shares: List[Tuple[int, bytes]] = []
    for i in range(1, n + 1):
        s_i = (i, eval_poly(c,i,prime))
        shares.append(s_i)
    return shares
def eval_poly(coefficients: List[bytes], x, prime):
    """ Evaluate polynomial of the finite field represented by the prime given. 
    """
    t = len(coefficients)
    if t == 0:
        return 0 
    y = int.from_bytes(coefficients[0],"big")
    xk=x
    for j in range(1,t):
        y = (y + int.from_bytes(coefficients[j],"big") * xk) % prime
        xk = (xk * x) % prime
    return y
In [5]:
bit_security=b*8 # bit length required for the prime number
randprime = int(random_prime(n=(2**bit_security)-1,proof=True,lbound=2**(bit_security-1)))
print(f"p as hex     = {hex(randprime)[2:]}")
print(f"p bit length = {randprime.bit_length()}")
assert randprime in Primes(),"Not a prime number"
assert int.from_bytes(s,'big') < randprime,"Prime must not be smaller than s"
p as hex     = da4de73dbe0ddf9107d5f56b50292635
p bit length = 128
In [6]:
print(f"Secret: \n\t{s.hex()}")
shares_sss = sss.share(s,5,3,randprime)
print(f"n shares: ")
for share in shares_sss:
    print(f"\t{share[0]}, {hex(share[1])[2:].zfill(b*2)}, {share[1].bit_length()}")
Secret: 
	9fd47c7bd94aeca621715e359135657c
n shares: 
	1, 778c6e15c4b5ae393cb8866b6d59b5a4, 127
	2, a181b808f8fb2c1cc34b679a4f28ff92, 128
	3, 43667317b80d86bfad540c56e67a1d11, 127
	4, 3788867fbffa9db302a86a0c83763456, 126
	5, 7de7f24110c270f6c34880bb261d4561, 127

Recovery over $\mathbb{F}_p$¶

This section describes how to reconstruct a secret from a set of (at least) $t$ given shares, computed with the method in Section "Sharing over $\mathbb{F}_p$". If all given shares are valid and a sufficient number of shares is provided, the secret can be recovered.

  • A share is considered valid, if it has its correct index ($i$) and the respective share value ($y_i$).
    • Note: If the index is wrong, the recovery will result in a wrong value! If the indices of shares are not known, the recovery might be computationally infeasable due to the large number of possible combinations when $ n $ is large.
  • The number of shares provided is sufficient, if it matches or exceeds the secret sharing threshold $t$ used during the sharing process.
    • Note: If $t$ is unknown and less than $t$ shares are provided, the recovery will result in a wrong value!

Input:

  • $\mathbf{\{s_1, s_2, \dots, s_{t}\}}$: A set of (at least) $t$ shares which should be used to recover a previously shared secret. Each share $s_i \mid 1 \leq i \leq t$ is given as a tuple $(x_i, y_i)$ of a share index $x_i$ (an integer from $1$ to $n$) and a share value $y_i$ (a sequence of $b$ bytes).

Output:

  • $\mathbf{s}$: The recovered secret.

Recover the secret $s$ over $\mathbb{F}_p$:

To recover the shared secret $s$ from the given set of shares $\{s_1, s_2, \dots, s_t\}$, the underlying secret sharing polynomial $f(\cdot)$ is evaluated using the Lagrange interpolation formula: $$ f(x) = \sum_{i = 1}^{t} y_i \cdot \ell_i(x) $$ Hereby, $\ell_i(x)$ is defined as follows and can also be used to reconstruct the coefficients. $$ \ell_i(x) = \prod_{\substack{j = 1 \\ i \not = j}}^{t} \frac{x - x_j}{x_i - x_j} $$ Since, we are only interested in reconstructing the secret $ s $, which is coefficient $c_0$, an optimized formla can be used which just computes this coefficient.

To recover the shared secret $s$ we apply this formula, which is the above formulat for the case $x = 0$: $$ s = c_0 = f(0) = \sum_{i = 1}^{t} y_i \prod_{\substack{j = 1 \\ i \not = j}}^{t} \frac{x_j}{x_j - x_i} $$ Note that there are two variables $i$ and $j$ for the indicies. One variable is for the outer sum ($i$) and one is for the inner product ($j$). If the outer index $i$ equals the inner index $ j$, i.e., $i = j$, then this step of the product calculation is omitted.

def recover(shares: Sequence[Tuple[int, bytes]], prime: int) -> bytes:
    """ Function for recovering a previously shared secret from a list of given shares.
    """
    if not (1 <= len(shares)):
        raise ValueError("Invalid number of shares provided.")

    if len({xi for xi, _ in shares}) != len(shares):
        raise ValueError("Invalid shares provided, duplicate share indices detected.")
    if not all(1 <= xi for xi, _ in shares):
        raise ValueError("Invalid shares provided, out of range share index/indices detected.")

    # Compute the secret via Lagrange interpolation
    s = 0
    for xi, yi in shares:
        li = 1
        for xj, _ in shares:
            if xi != xj:
                # ( x_j / (x_j - x_i) ) * ...
                li_current = (xj * pow((xj - xi),-1,prime)) % prime
                li = (li * li_current) % prime
        s = (s + yi * li) % prime

    # return the reconstructed secret
    return s
In [7]:
s_recovered = sss.recover(shares_sss[:3],randprime)
assert s.hex() == hex(s_recovered)[2:],"Recovery failed!"
print(f"Recovered secret: \n\t{hex(s_recovered)[2:]}")
Recovered secret: 
	9fd47c7bd94aeca621715e359135657c

Sharing secret $ s $ byte-per-byte over $\text{GF}(256)$¶

In the following, we specify the process of creating $1 \leq n \leq 255$ shares $(s_1, s_2, ..., s_n)$ for a given secret $s$ (a sequence of $b \geq 16$ bytes) such that any subset of at least $1 \leq t \leq n$ of those shares can be used to recover the shared value.

Input:

  • $\mathbf{s}$: The secret to be shared, i.e., a sequence of $b$ bytes, where $b \geq 16$.
  • $\mathbf{n}$: The number of shares to be generated, $1\leq n \leq 255$.
  • $\mathbf{t}$: The secret sharing threshold, $1 \leq t \leq n$. This value specifies the minimum number of shares required for the recovery process.

Output:

  • $\mathbf{s_1, s_2, \dots, s_n}$: A list of $n$ shares. Each share $s_i$ is represented as a tuple $(x_i, y_i)$ consisting of the share index $x_i = i$ (an integer from $1$ to $255$) and the share value $y_i$ (a sequence of $b$ bytes).

Note that typical values for $n$ and $t$ are in the range of $2 \leq t \leq 255$ . If $t = 1$, the number of shares required for the recovery is one. In this case, each share created is just the value being shared. If $t = n$, all created shares are required for the recovery process. If, for example, $n = 5$ and $t = 3$, then $5$ shares are created in total. and each combination of at least $3$ shares can be used to recover the shared secret.

Sharing over $\text{GF}(256)$¶

The process of secret sharing is executed on a byte-per-byte basis, i.e., each of the $b$ bytes of the secret $s$ to be shared is processed individually. This allows for a simple and efficient implementation using the finite field $\text{GF}(2^8)$ with 256 elements. In this field addition and subtraction of two field elements (two bytes) are defined using the bitwise xor operation. Multiplication of two field elements (again two bytes) is defined using the AES reducing polynomial $x^8 + x^4 + x^3 + x + 1$. For additional background information regarding finite field arithmetic we refer the reader to the Wikipedia page on Finite Field Arithmetic. The used secret sharing polynomials $f_0(\cdot), f_1(\cdot), \dots, f_{b-1}(\cdot)$ of degree $t - 1$ are evaluated over $\text{GF}(2^8)$: $$ f_k(x) = c_0[k] + c_1[k] \cdot x + c_2[k] \cdot x^2 + \dots + c_{t - 1}[k] \cdot x^{t - 1}, \quad 0 \leq k < b. $$ Note that here the values of $c_0, c_1, \dots, c_{t - 1}$ are used to represent lists of coefficients, whereas the $k^\textsf{th}$ element of each of the lists of coefficients is used to define the secret sharing polynomial for sharing the $k^\textsf{th}$ byte of the secret $s$.

Each list of coefficients $c_j \mid 0 \leq j < t$ is defined as follows: $$ c_j = \begin{cases} s &\text{if } j = 0 \\ \textsf{random}(b) &\text{if } 0 < j < t \\ \end{cases} $$ Hereby, the function $\textsf{random}(b)$ creates a list of $b$ random bytes. The function MUST use a cryptographically secure source of randomness.

Given this definition of coefficients (and thus of the secret sharing polynomials), the $n$ shares $s_1, s_2, ..., s_n$ are computed as follows: $$ s_i = \left( i, f_0(i) \mid\mid f_1(i) \mid\mid \dots \mid\mid f_{b-1}(i) \right) \quad 1 \leq i \leq n $$ Note that a share is a tuple of two values and MUST include the share index $i$ as well as the share value, obtained by evaluating the secret sharing polynomials and concatenating the results.

def share(s: bytes, n: int, t: int) -> List[Tuple[int, bytes]]:
    """ Main function for creating `n` shares of a secret `s` such that any subset of at least `t` generated shares
    can be used to recovered the secret `s`.
    """
    b = len(s)
    if not (1 <= t <= n <= 255):
        raise ValueError("Invalid secret sharing parameters, ensure that 1 <= t <= n <= 255 holds.")

    c: List[bytes] = [b""] * t
    for j in range(t):
        if j == 0:
            c[j] = s
        elif 0 < j < t:
            c[j] = random(b)
        else:
            raise ValueError("Invalid range for t.")

    f: List[GF256.Polynomial] = []
    for k in range(b):
        f_k = GF256.Polynomial([c[j][k] for j in range(t)])
        f.append(f_k)

    shares: List[Tuple[int, bytes]] = []
    for i in range(1, n + 1):
        s_i = (i, bytes(f[k](i) for k in range(b)))
        shares.append(s_i)

    return shares
In [13]:
print(f"Secret: \n\t{s.hex()}")
shares_sssgf = sssgf.share(s,5,3)
print(f"n shares: ")
for share in shares_sssgf:
    print(f"\t{share[0]},{share[1].hex()}")
Secret: 
	9fd47c7bd94aeca621715e359135657c
n shares: 
	1,4f004a9700d65ee1f526096170e191ba
	2,f905823317904f1b1688084a3964370a
	3,29d1b4dfce0cfd5cc2df5f1ed8b0c3cc
	4,0afee5f1d92cec0b69df3fba05327339
	5,da2ad31d00b05e4cbd8868eee4e687ff

Recovery over $\text{GF}(256)$¶

This section describes how to reconstruct a secret from a set of $t$ given shares, computed with the method specified in Section Sharing over $\text{GF}(256)$. Implementations MUST be able to successfully recover the shared secret $s$, if all given shares are valid and a sufficient number of shares is provided.

  • A share is considered valid, if it was generated according this specification. Therefore, it has to consist of its correct index ($i$) and the respective share value ($y_i$).
    • Note: If the index is wrong, the recovery will result in a wrong value! If the indices of shares are not known, the recovery might be computationally infeasable.
  • The number of shares provided is sufficient, if it matches or exceeds the secret sharing threshold $t$ used during the sharing process.
    • Note: If $t$ is unknown and less than $t$ shares are provided, the recovery will result in a wrong value!

Input:

  • $\mathbf{\{s_1, s_2, \dots, s_{t}\}}$: A set of $t$ (at least) shares which should be used to recover a previously shared secret. Each share $s_i \mid 1 \leq i \leq t$ is given as a tuple $(x_i, y_i)$ of a share index $x_i$ (an integer from $1$ to $255$) and a share value $y_i$ (a sequence of $b$ bytes).

Output:

  • $\mathbf{s}$: The recovered secret.

Using $\text{GF}(2^8)$ for byte-per-byte recovery of the secret $s$:

To recover the shared secret $s$ from the given set of shares $\{s_1, s_2, \dots, s_t\}$, the underlying secret sharing polynomials $f_k(\cdot)$ are evaluated using the Lagrange interpolation formula: $$ f_k(x) = \sum_{i = 1}^{t} y_i[k] \cdot \ell_i(x), \quad 0 \leq k < b $$ $$ \ell_i(x) = \prod_{\substack{j = 1 \\ i \not = j}}^{t} \frac{x - x_j}{x_i - x_j} $$

To recover the shared secret $s$ we apply the above formula for $x = 0$: $$ s[k] = c_0[k] = f_k(0) = \sum_{i = 1}^{t} y_i[k] \prod_{\substack{j = 1 \\ i \not = j}}^{t} \frac{x_j}{x_j - x_i}, \quad 0 \leq k < b. $$ Neglecting the notational differences covering the byte-per-byte execution of the process, this approach is essentially equal to Shamir's original description.

def recover(shares: Sequence[Tuple[int, bytes]]) -> bytes:
    """ Main function for recovering a previously shared secret from a list of given shares.
    """
    if not (1 <= len(shares) <= 255):
        raise ValueError("Invalid number of shares provided.")

    if len({xi for xi, _ in shares}) != len(shares):
        raise ValueError("Invalid shares provided, duplicate share indices detected.")
    if not all(1 <= xi <= 255 for xi, _ in shares):
        raise ValueError("Invalid shares provided, out of range share index/indices detected.")

    b = len(shares[0][1])
    if any(len(yi) != b for _, yi in shares):
        raise ValueError("Invalid shares provided, share lengths are inconsistent.")

    # Compute the secret via Lagrange interpolation.
    s = bytearray(b)
    for xi, yi in shares:
        li = 1
        for xj, _ in shares:
            if xi != xj:
                li = GF256.multiply(li, GF256.multiply(xj, GF256.inverse(xj ^ xi)))
        for k in range(b):
            s[k] ^= GF256.multiply(yi[k], li)
    s = bytes(s)

    # return the reconstructed secret
    return s
In [14]:
s_recovered = sssgf.recover(shares_sssgf)
assert s == s_recovered
print(f"Recovered secret: \n\t{s_recovered.hex()}")
Recovered secret: 
	9fd47c7bd94aeca621715e359135657c
In [ ]:
 
In [ ]:
 
In [ ]: