Files
crypto/docs/threshold/secret-sharing.mdx
T

344 lines
14 KiB
Plaintext
Raw Normal View History

2026-09-02 15:29:51 -04:00
---
title: Secret Sharing
description: Shamir, Feldman, and Pedersen verifiable secret sharing over any supported curve — plus the legacy sharing/v1 layer and its known defects.
sidebar:
order: 2
icon: split
---
The `sharing` package splits a curve scalar into `n` shares such that any `t` of them reconstruct
it, and fewer than `t` reveal nothing. All three schemes share one share type and one
reconstruction routine; they differ only in what a shareholder can *verify* about the share it was
handed.
This is dealer-based sharing: one process holds the secret while `Split` runs. If you need a key
that never exists in one place, you want [DKG](/threshold/dkg) instead.
## Picking a scheme
<CardGroup cols={3}>
<Card title="Shamir" icon="split">
No verification. Fastest, smallest. Use only when every shareholder is trusted, or when a higher
layer verifies for you.
</Card>
<Card title="Feldman" icon="eye">
Adds polynomial commitments `a_j · G`. A holder can check its own share against them. The
commitments leak `a_0 · G` — i.e. the public key of the secret.
</Card>
<Card title="Pedersen" icon="eye-off">
Feldman plus a blinding polynomial under a second generator `H`. The commitments are
information-theoretically hiding, so nothing about the secret leaks before reconstruction.
</Card>
</CardGroup>
## Shamir
```go
import (
crand "crypto/rand"
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/sharing"
)
func shamirRoundTrip() error {
curve := curves.ED25519()
scheme, err := sharing.NewShamir(3, 5, curve) // 3-of-5
if err != nil {
return err
}
secret := curve.Scalar.Hash([]byte("test"))
shares, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
// Any 3 of the 5 shares reconstruct the secret.
recovered, err := scheme.Combine(shares[0], shares[2], shares[4])
if err != nil {
return err
}
_ = recovered.Cmp(secret) // == 0
return nil
}
```
`Combine` reconstructs the scalar. `CombinePoints` does the same interpolation in the group,
returning `secret · G` — useful when you want to check that a share set corresponds to a known
public key without materialising the key. `Shamir.LagrangeCoeffs(identities []uint32)` returns the
interpolation coefficients on their own, keyed by identifier, so a caller can compute
`Σ λ_i · share_i` itself; this is exactly what [`ted25519/frost`](/threshold/threshold-ed25519)
needs.
:::info[LagrangeCoeffs has two different signatures]
`Shamir.LagrangeCoeffs` takes `identities []uint32`. `Feldman.LagrangeCoeffs` and
`Pedersen.LagrangeCoeffs` take `shares map[uint32]*ShamirShare` and then delegate to the Shamir
implementation — same computation, different argument shape. Note the subtlety: they read the id
from each map *value* (`share.Id`), never from the map key, so a mismatched key is silently
ignored and a nil value panics. `Combine` and `CombinePoints` likewise delegate, so all three
types behave identically there.
:::
:::danger[Shamir has no integrity check whatsoever]
`Combine` validates that each share's id is nonzero and within `limit`, that ids are not duplicated,
and that the value is a nonzero scalar. It does **not** and cannot check that a share lies on the
dealer's polynomial. One malicious shareholder submitting a well-formed but wrong `Value` silently
produces a wrong secret, with no error. If shareholders are not mutually trusted, use Feldman or
Pedersen and verify every share before combining.
:::
## Feldman
`Feldman.Split` returns a `*FeldmanVerifier` alongside the shares. The verifier holds `Threshold`
points — `a_j · G` for each polynomial coefficient — and `Verify` recomputes
`Σ a_j · id^j` and compares it to `share · G`.
```go
scheme, err := sharing.NewFeldman(3, 5, curves.ED25519())
if err != nil {
return err
}
verifier, shares, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
for _, s := range shares {
if err := verifier.Verify(s); err != nil { // nil == valid
return err
}
}
recovered, err := scheme.Combine(shares[0], shares[1], shares[2])
```
`Verify` returns `fmt.Errorf("not equal")` on a mismatch — there is no typed sentinel error, so
compare against `nil` rather than matching the message.
:::info[The verifier is public data, and it publishes the public key]
`Commitments[0]` *is* `secret · G`. Distributing a `FeldmanVerifier` therefore discloses the public
key of the shared secret. That is normally what you want for a signing key. It is not what you want
if the shared secret must stay hidden even in the exponent — use Pedersen.
:::
## Pedersen
`NewPedersen` takes a **generator point** rather than a curve; the curve is derived from
`generator.CurveName()`. `Split` returns a single struct carrying both verifiers and both share
sets.
```go
curve := curves.ED25519()
// H must have unknown discrete log with respect to G.
h := curve.Point.Generator().Hash([]byte("sonr/pedersen/H/v1"))
scheme, err := sharing.NewPedersen(3, 5, h)
if err != nil {
return err
}
result, err := scheme.Split(secret, crand.Reader)
if err != nil {
return err
}
for i := range result.SecretShares {
// Pedersen verification needs BOTH the secret share and its blinding share.
err = result.PedersenVerifier.Verify(result.SecretShares[i], result.BlindingShares[i])
if err != nil {
return err
}
// The Feldman verifier is also returned and checks the secret share alone.
if err = result.FeldmanVerifier.Verify(result.SecretShares[i]); err != nil {
return err
}
}
recovered, err := scheme.Combine(result.SecretShares[0], result.SecretShares[1], result.SecretShares[2])
```
<TypeTable
type={{
Blinding: {
type: "curves.Scalar",
required: true,
description: "The blinding factor's intercept. Secret — leaking it collapses Pedersen to Feldman.",
},
SecretShares: {
type: "[]*ShamirShare",
required: true,
description: "Shares of the secret. Length == limit.",
},
BlindingShares: {
type: "[]*ShamirShare",
required: true,
description: "Shares of the blinding polynomial, index-aligned with SecretShares.",
},
FeldmanVerifier: {
type: "*FeldmanVerifier",
required: true,
description: "Unblinded commitments a_j · G. Reveals the public key.",
},
PedersenVerifier: {
type: "*PedersenVerifier",
required: true,
description: "Blinded commitments a_j · G + b_j · H, plus the generator H.",
},
}}
/>
:::danger[The generator must have unknown discrete log relative to the base point]
Pedersen's hiding property rests on nobody knowing `x` with `H = x · G`. If the dealer picks
`H = x · G` for a known `x`, it can open the commitment `a_j · G + b_j · H` to any value it likes:
the commitment stops being binding, so the dealer can hand out shares of one secret and later prove
they were shares of another. `NewPedersen` cannot detect this — it only checks that the generator is
on the curve and is not the identity. Derive `H` by hashing to the curve (as above), or use a
published nothing-up-my-sleeve constant. Never derive it as a scalar multiple of `G`.
:::
## ShamirShare
All three schemes emit the same share type.
<TypeTable
type={{
Id: {
type: "uint32",
required: true,
description: "The x-coordinate. 1-indexed; 0 is rejected. Must be ≤ limit.",
},
Value: {
type: "[]byte",
required: true,
description: "The y-coordinate, as the curve's canonical scalar encoding.",
},
}}
/>
`Bytes()` returns the id as 4 big-endian bytes followed by `Value` — a stable wire form.
`Validate(curve)` rejects a zero id, a `Value` that does not decode as a scalar on `curve`, and a
zero scalar. The struct carries `json` tags (`identifier`, `value`) and round-trips through
`encoding/json`.
## Constructor constraints
Identical across `NewShamir`, `NewFeldman`, and `NewPedersen`, checked in this order:
| Check | Error |
| --- | --- |
| `limit >= threshold` | `limit cannot be less than threshold` |
| `threshold >= 2` | `threshold cannot be less than 2` |
| `limit <= 255` | `cannot exceed 255 shares` |
| curve resolvable / non-nil | `invalid curve` |
`Shamir.Split` and `Feldman.Split` additionally reject a zero secret with `invalid secret`.
:::warning[Two rough edges in the constructors]
`Pedersen.Split` reaches the shared polynomial helper directly and **skips the zero-secret check**,
so `NewPedersen(...).Split(curve.Scalar.Zero(), rand)` succeeds and produces shares of zero. Check
`secret.IsZero()` yourself.
`NewPedersen` calls `generator.CurveName()` *before* its `generator == nil` guard, so passing a nil
generator panics with a nil-pointer dereference instead of returning `invalid generator`.
:::
## The Polynomial degree gotcha
`sharing.Polynomial` is exported, and its `Init` signature reads as if it takes a degree:
```go
func (p *Polynomial) Init(intercept curves.Scalar, degree uint32, reader io.Reader) *Polynomial
```
It does not. The implementation allocates `degree` coefficients — `Coefficients[0] = intercept` plus
`degree - 1` random ones — so the resulting polynomial has algebraic degree `degree - 1`. The
parameter is really a *coefficient count*.
Inside the package this is consistent: every scheme calls `Init(secret, threshold, reader)`, which
yields `threshold` coefficients and hence degree `threshold - 1` — precisely what a `t`-of-`n`
scheme requires. But if you call `Init` yourself expecting the named semantics you will get a
polynomial one degree lower than you asked for, and `Init(x, 0, r)` panics on
`Coefficients[0]` before it can return an error.
:::tip
Use `NewShamir`/`NewFeldman`/`NewPedersen`. `Polynomial` is exported incidentally, not as a
supported API.
:::
## Legacy: sharing/v1
<Badge variant="warning">Legacy — do not use for new code</Badge>
`sharing/v1` is the pre-`curves.Curve` generation of the same three schemes. It operates on
`[]byte` secrets over `curves.Field`/`curves.Element` and uses `curves.EcPoint` (aliased locally as
`ShareVerifier`) rather than `curves.Point`. It survives because [`dkg/gennaro`](/threshold/dkg) and
[`ted25519/ted25519`](/threshold/threshold-ed25519) are built on it and have never been ported.
Differences that matter if you must read it:
- `v1.NewShamir(threshold, limit int, field *curves.Field)` takes plain `int`s and a *field*, not a
curve. It enforces only `limit >= threshold` and `threshold >= 2` — **no 255-share ceiling**.
- `v1.NewFeldman(threshold, limit uint32, curve elliptic.Curve)` and
`v1.NewPedersen(threshold, limit uint32, generator *curves.EcPoint)` take standard-library
curves. Tests drive them with `btcec.S256()` and `elliptic.P256()`.
- `Split` takes `[]byte` and reads randomness from an internal source — there is no `io.Reader`
parameter, so you cannot inject a deterministic RNG.
- `Verify` returns `(bool, error)` rather than a bare `error`, and the verifier list is a plain
slice you pass in, not a struct.
- `ShamirShare` here has fields `Identifier uint32` and `Value *curves.Element`, and gains an
`Add` method that panics if the two identifiers differ.
- `ComputeL` is the `LagrangeCoeffs` equivalent, returning an ordered `[]*curves.Element`.
Curve helpers provided by the package: `Ed25519()`, `Bls12381G1()`, `Bls12381G2()`, and
`K256GeneratorFromHashedBytes(bytes []byte) (x, y *big.Int, err error)` — which derives a generator
with unknown discrete log from a byte string, exactly the Pedersen requirement above. There is no
k256 or p256 curve constructor in `v1`; use `btcec.S256()` and `elliptic.P256()` directly, as the
tests do.
:::danger[v1.Bls12381G2 does not return a G2 curve]
Despite its name and its `*Bls12381G1Curve` return type, `Bls12381G2()` initialises a
`Bls12381G2Curve` singleton and then returns `&bls12381g1` — the **G1** curve. Its initialiser also
sets `Name = "Bls12381G1"`, and its `Gy` is a verbatim copy of `B`. The `Bls12381G2Curve` methods
(`Add`, `Double`, `ScalarMult`, `IsOnCurve`, `Hash`) do operate on real G2 points, but there is no
exported constructor that hands you a value of that type. Do not use `Bls12381G2()`.
:::
:::warning[v1.Shamir.Combine ignores shares past the threshold]
`Combine` loops `for i := 0; i < int(s.threshold); i++` over the variadic slice, so passing five
shares to a 3-of-5 scheme interpolates the **first three** and discards the rest. Combined with
`v1`'s lack of Feldman checking inside `Combine`, a corrupt share in one of the leading positions
silently poisons the result even when enough good shares were supplied to notice. The same slicing
applies to `ComputeL`. Note the contrast: the modern `sharing.Shamir.Combine` interpolates over
*all* shares you pass.
:::
## Caveats
:::warning[Not constant time]
Reconstruction goes through `curves.Scalar` arithmetic and, in `v1`, `big.Int`-backed
`curves.Element` arithmetic. Neither is written for constant-time operation on secret data. Treat
these routines as unsafe against local timing adversaries.
:::
:::note[Reconstruction is the dangerous moment]
`Combine` materialises the secret in memory. Any design where `Combine` runs in production has a
window where the key exists in one place — the thing threshold cryptography is meant to eliminate.
Prefer schemes where shares are consumed *as shares* (`LagrangeCoeffs` plus threshold signing) over
schemes that reassemble.
:::
## Next
<CardGroup cols={2}>
<Card title="Distributed Key Generation" href="/threshold/dkg" icon="git-branch">
Same VSS machinery, but nobody ever holds the secret.
</Card>
<Card title="Curves & Scalars" href="/foundations/curves" icon="binary">
The `Curve`, `Point`, and `Scalar` abstractions every scheme here is generic over.
</Card>
</CardGroup>