--- 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 No verification. Fastest, smallest. Use only when every shareholder is trusted, or when a higher layer verifies for you. 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. Feldman plus a blinding polynomial under a second generator `H`. The commitments are information-theoretically hiding, so nothing about the secret leaks before reconstruction. ## 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]) ``` :::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. `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 Legacy — do not use for new code `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 Same VSS machinery, but nobody ever holds the secret. The `Curve`, `Point`, and `Scalar` abstractions every scheme here is generic over.