Additive Secret Sharing for Coordinates in Python

Additive secret sharing is the workhorse of privacy-preserving spatial aggregation: a quantized coordinate is split into n random shares that sum to the secret modulo a prime p, and reconstruction requires every one of them. It is the cheap, non-threshold cousin of the polynomial scheme in the parent guide on secret sharing for coordinates — no Lagrange interpolation, no field inversion, just modular addition — which is exactly why it, and not Shamir secret sharing for GPS coordinate protection, is what backs high-throughput share-and-sum protocols across the Secure Multi-Party Computation in Spatial Analytics architecture. This page is the narrow, implementation-first treatment of the n-of-n additive construction over Zp\mathbb{Z}_p: how it differs from a tt-of-nn threshold, how to size the modulus for a summation rather than a single value, and how to add shares locally so an aggregator learns a regional total without ever seeing a coordinate.

Parameter Configuration and Calibration

Additive sharing trades Shamir’s fault tolerance for speed and composability. Where the threshold scheme lets any tt of nn shares rebuild the secret, an additive split of ss into s1,,sns_1, \dots, s_n with isis(modp)\sum_i s_i \equiv s \pmod p is strictly n-of-n: drop a single share and the secret is information-theoretically gone. That property is a liability for storage but an asset for summation, because shares add coordinate-wise — party jj can fold the jj-th share of a thousand records into one running total without ever talking to another party. This is the primitive underneath federated secure aggregation protocols, where the server must learn only the sum of client contributions.

Four knobs govern a correct deployment. Each is a correctness or privacy lever, not a performance dial.

Parameter Additive choice Rationale
num_parties (n) one per independent trust domain No threshold exists, so every party is load-bearing; n is bounded by how many independent custodians you can actually run.
modulus (p) 26112^{61}-1 (Mersenne prime) Must exceed the largest possible aggregate, not one coordinate — see below. A Mersenne prime makes reduction a shift-and-add.
SCALE 10610^6 (microdegrees, ~0.11 m) Fixed-point integers keep addition exact; floats do not. Coarser than the telemetry guide’s nanodegrees because sums, not single fixes, dominate here.
per-axis handling latitude and longitude shared separately Independent randomness per axis stops recovery of one axis from constraining the other.

The modulus rule is the one that catches people migrating from Shamir. There, pp only has to exceed a single biased coordinate plus its random coefficients. In additive share-and-sum, reconstruction is isimodp\sum_i s_i \bmod p, so if the true aggregate of NN quantized coordinates exceeds pp it silently wraps and the recovered total is wrong by a multiple of pp. Size pp so that

p  >  Nmaxqmax,qmax=360×1063.6×108,p \;>\; N_{\max} \cdot q_{\max}, \qquad q_{\max} = 360 \times 10^{6} \approx 3.6\times 10^{8},

where NmaxN_{\max} is the largest number of records you will ever sum. With p=26112.3×1018p = 2^{61}-1 \approx 2.3\times10^{18} you can aggregate well over 10910^{9} coordinates before the field boundary is in play — ample for city- or nation-scale density counts, and small enough that shares are single 64-bit integers on the wire. Unlike Shamir, additive sharing does not require a prime at all (any modulus, including 2k2^{k}, gives a perfectly uniform share distribution); a prime is chosen only so the same field interoperates with multiplicative sub-protocols and matches the rest of the pipeline.

Reference Implementation

The AdditiveSharing class below is pure standard-library Python: secrets.randbelow for cryptographically uniform shares and integer arithmetic for everything else. It exposes exactly three operations — split, reconstruct, and the secure_sum that is the reason to reach for this scheme. Inline comments flag the lines where a choice has a privacy consequence.

python
from __future__ import annotations

import secrets
from typing import List, Sequence

SCALE: int = 10 ** 6          # microdegrees, ~0.11 m at the equator
BIAS: int = 180 * SCALE       # shift [-180, 180] into non-negative field elements


def quantize(deg: float) -> int:
    """Map one WGS84 axis (decimal degrees) to a non-negative integer in Z_p.

    Fixed-point integers make modular addition exact; sharing raw IEEE 754
    floats would make the share sum non-deterministic and leak sub-metre
    artefacts that can fingerprint a location.
    """
    if not (-180.0 <= deg <= 180.0):
        raise ValueError(f"axis out of WGS84 range: {deg}")
    return round(deg * SCALE) + BIAS


class AdditiveSharing:
    """n-of-n additive secret sharing over Z_p.

    A quantized coordinate `s` is split into `n` shares whose sum is
    congruent to `s` mod p. Each share is uniform on its own, so any
    strict subset of the n shares reveals nothing about `s`. Unlike a
    Shamir t-of-n threshold scheme there is NO fault tolerance: all n
    shares are required, and one lost share makes `s` unrecoverable.
    """

    def __init__(self, num_parties: int, modulus: int = (1 << 61) - 1) -> None:
        if num_parties < 2:
            raise ValueError("additive sharing needs at least 2 parties")
        if modulus <= 0:
            raise ValueError("modulus must be positive")
        self.n: int = num_parties
        self.p: int = modulus

    def split(self, secret: int) -> List[int]:
        """Split one quantized coordinate into n additive shares.

        The first n-1 shares are drawn uniformly from Z_p with a CSPRNG;
        the final share is fixed to close the sum. That uniform sampling
        is the entire security argument — never seed it from a coordinate.
        """
        if not (0 <= secret < self.p):
            raise ValueError("secret must lie in [0, p)")
        shares: List[int] = [secrets.randbelow(self.p) for _ in range(self.n - 1)]
        last: int = (secret - sum(shares)) % self.p   # closes sum to s mod p
        shares.append(last)
        return shares

    def reconstruct(self, shares: Sequence[int]) -> int:
        """Recover the secret. Requires ALL n shares — there is no threshold."""
        if len(shares) != self.n:
            raise ValueError(
                f"n-of-n scheme: need all {self.n} shares, got {len(shares)}"
            )
        return sum(shares) % self.p

    def secure_sum(self, share_vectors: Sequence[Sequence[int]]) -> int:
        """Aggregate several secrets from their shares without reconstructing any.

        `share_vectors[k]` is the n-share vector of secret k. Party j adds
        the j-th share of every secret locally (a column sum); only those n
        party-local partial sums are ever revealed, and their total equals
        the plaintext sum of the secrets mod p. No single coordinate is
        materialised at any point in the protocol.
        """
        partial_sums: List[int] = [0] * self.n
        for vec in share_vectors:
            if len(vec) != self.n:
                raise ValueError("every share vector must hold n shares")
            for j in range(self.n):
                # this update happens ON party j's node — shares never pool centrally
                partial_sums[j] = (partial_sums[j] + vec[j]) % self.p
        return sum(partial_sums) % self.p

The design keeps the secure_sum reduction explicit so it maps onto real infrastructure: in production the inner partial_sums[j] update executes on party jj’s own node, and only the nn scalars partial_sums cross the network. Because those partials are themselves an additive sharing of the aggregate, revealing all nn of them discloses only the total — the same reason a lone share discloses nothing about a single record.

Additive secret sharing — split one coordinate into n shares, then share-and-sum to a revealed aggregate A two-band diagram. The top band, "Split one coordinate", shows a box for the quantized coordinate s feeding an additive-split box that draws r1 through r_{n-1} uniformly from Z_p and sets the final share r_n = s minus the sum of the others mod p. Fan-out arrows carry one share each to a vertical bank of parties: Party 1 holds r1, Party 2 holds r2, and Party n holds r_n. A caption notes all n shares are required because they sum to s mod p, while any n-1 are uniform and reveal nothing. The bottom band, "Secure sum", stacks Party 1, Party 2, and Party n on the left, each labelled as adding the shares it holds across many records into a local partial sum u1, u2, u_n. Fan-in arrows labelled "reveal only partial sums" converge on an aggregator box on the right that computes the sum of u_j mod p, equal to the plaintext sum of every coordinate, with individual coordinates never revealed. Additive secret sharing — split into n shares, then share-and-sum

① Split one quantized coordinate — all n shares required

Coordinate s quantized ∈ [0, p) Additive split r₁ … r₍ₙ₋₁₎ ← Z_p rₙ = s − Σrᵢ mod p one share each Party 1 · holds r₁ Party 2 · holds r₂ Party n · holds rₙ n-of-n, no threshold Σ rᵢ ≡ s (mod p) — all n needed any n−1 shares are uniform: consistent with every coordinate one lost share → unrecoverable

② Secure sum — parties add shares locally, only the total is revealed

Party 1 sums its shares over all records → u₁ Party 2 sums its shares over all records → u₂ Party n sums its shares over all records → uₙ reveal only partial sums uⱼ Aggregator Σⱼ uⱼ mod p = Σₖ sₖ plaintext total of every coordinate no individual coordinate revealed

Validation Checkpoint

Additive sharing fails silently on two axes — a wrapped aggregate returns a plausible wrong total, and a mis-sampled share weakens the guarantee without raising — so the invariants below belong in CI, not in a manual review. The harness reuses the class above.

python
def _validate() -> None:
    p: int = (1 << 61) - 1
    scheme = AdditiveSharing(num_parties=4, modulus=p)

    # 1. Shares sum to the secret mod p, at the field edges and a real point.
    for secret in (0, 1, p - 1, quantize(40.748817), quantize(-73.985428)):
        shares = scheme.split(secret)
        assert len(shares) == 4
        assert scheme.reconstruct(shares) == secret
        assert sum(shares) % p == secret

    # 2. n-of-n: any n-1 shares must NOT reconstruct (there is no threshold).
    shares = scheme.split(123_456_789)
    try:
        scheme.reconstruct(shares[:-1])
    except ValueError:
        pass
    else:
        raise AssertionError("n-1 shares must not reconstruct")

    # 3. The first n-1 shares are uniform and independent of the secret:
    #    two different secrets can be split under an identical (n-1)-prefix,
    #    with only the closing share differing. This is why a strict subset
    #    leaks nothing — it is consistent with every possible coordinate.
    prefix = [secrets.randbelow(p) for _ in range(scheme.n - 1)]  # any uniform prefix
    for target in (quantize(51.5074), quantize(-0.1278)):
        closing = (target - sum(prefix)) % p                 # unique closing share
        assert scheme.reconstruct(prefix + [closing]) == target

    # 4. secure_sum over several coordinates equals the plaintext sum mod p,
    #    without any coordinate being reconstructed.
    plain = [quantize(v) for v in (40.7128, 34.0522, 51.5074, -33.8688)]
    vectors = [scheme.split(s) for s in plain]
    assert scheme.secure_sum(vectors) == sum(plain) % p

    print("all additive-sharing invariants hold")


if __name__ == "__main__":
    _validate()

Assertion 3 is the structural heart of the scheme: any n1n-1 shares can be extended to any secret by an appropriate closing share, so they carry zero information. Assertion 4 confirms the share-and-sum property that makes additive sharing worth using — and if it ever fails while individual reconstruct calls pass, suspect a modulus too small for the aggregate.

Incident Response and Edge Cases

Additive sharing has fewer moving parts than Shamir, and its failure modes cluster around its two defining traits: strict n-of-n recovery and modular arithmetic on sums.

  • Lost or unavailable share. A single offline party makes the secret and every aggregate that includes it unrecoverable — there is no interpolation to route around the gap. Detection: fewer than n acknowledgements at the round timeout. Remediation: additive sharing is the wrong primitive if availability matters; either fall back to a Shamir (t,n)(t,n) layer for at-rest storage, or apply SecAgg-style pairwise dropout masks so a late party’s contribution can be recovered by the survivors, as covered under secure aggregation protocols.
  • Modulus overflow on the aggregate. Summing more records than p/qmaxp / q_{\max} wraps the total; reconstruct and secure_sum return a value off by a multiple of pp with no error. Detection: an aggregate whose de-quantized value falls outside the plausible coordinate-sum range. Remediation: raise pp so p>Nmaxqmaxp > N_{\max}\cdot q_{\max}, or shard the sum into bounded batches whose partial totals cannot each wrap.
  • Negative-bias sign error. Coordinates that skip the BIAS offset enter the field as small residues near zero for southern/western points, and their additive shares still sum correctly — but de-quantizing the aggregate double-counts or drops the bias, skewing the mean. Detection: assert every secret is in [0,p)[0,p) after quantize, and subtract exactly NBIASN\cdot\text{BIAS} when converting a sum of NN coordinates back to degrees. Remediation: centralise bias handling in quantize/de-quantize and unit-test the southern-hemisphere sum.
  • Weak randomness on the first shares. Seeding split from anything but a CSPRNG (a PRNG seeded by a timestamp, a reused nonce) makes the n1n-1 shares predictable and collapses the information-theoretic guarantee. Detection: audit the entropy source feeding split. Remediation: keep secrets.randbelow (or an HSM DRBG); never derive share randomness from the coordinate itself.

Frequently Asked Questions

When should I use additive sharing instead of Shamir's threshold scheme?

Use additive n-of-n sharing when the operation is summation across many parties or records — density counts, federated gradient aggregation, regional totals — because shares add locally and reconstruction is a single modular sum with no interpolation. Use the Shamir t-of-n threshold when you need fault-tolerant storage or access control, where a quorum smaller than n must be able to reconstruct. The two often compose: additive sharing for the hot aggregation path, Shamir for durable custody of the same secret.

Why does additive sharing have no fault tolerance, and how do I mitigate a lost share?

Because the shares are constrained only by summing to the secret mod p, any n-1 of them are jointly uniform and pin down nothing — which also means the missing share cannot be inferred. There is no polynomial to interpolate through the gaps as in Shamir. Mitigate it at the protocol layer: use SecAgg pairwise masks so surviving parties can cancel a dropped client's contribution, keep a separate Shamir copy for at-rest recovery, or provision enough redundancy that a round can simply be retried.

Does additive secret sharing need a prime modulus?

No. Additive sharing is uniform over any modulus, including a power of two, because the security argument only needs the first n-1 shares drawn uniformly at random. A prime — a Mersenne prime such as 2^61 - 1 for cheap reduction — is chosen so the same field interoperates with multiplicative MPC sub-protocols and with a Shamir layer that genuinely requires a field for Lagrange inversion, not because addition needs it.

How large must the modulus be for a secure sum of coordinates?

Large enough that the true aggregate never exceeds it: p must be greater than the maximum record count times the maximum biased quantized coordinate. With microdegree scaling a coordinate is under 3.6e8, so p = 2^61 - 1 safely sums over a billion coordinates before wrapping. If the plaintext total ever exceeds p it wraps modulo p and the reconstructed sum is silently wrong, so size p against your worst-case batch, not against a single value.

Up: Secret Sharing for Coordinates · Secure Multi-Party Computation in Spatial Analytics