Secure Aggregation Protocols

A federated server training a road-network model across thousands of mobile mapping devices needs one thing from each round: the sum of the clients’ gradient updates. It must never see any single update, because one device’s spatial gradient encodes the exact tiles it drove — a raw update is close enough to a trajectory to pin the vehicle to a neighbourhood, sometimes to a driveway. Secure aggregation (SecAgg) is the protocol that resolves this tension: every client blinds its update with additive masks that are constructed to cancel out precisely when the server adds the masked updates together, so the server learns ixi\sum_i x_i and provably nothing about any individual xix_i. This guide covers the pairwise-masking construction, the Diffie–Hellman seeding that makes it non-interactive, the Shamir-backed dropout recovery that keeps a round alive when devices vanish, the modular integer arithmetic that makes the masks cancel exactly, and how the whole thing composes with differential privacy.

This guide sits inside the Federated Learning Workflows for Geospatial Data architecture. Where gradient aggregation techniques decide how updates are weighted and combined, secure aggregation decides who gets to see them on the way in — the server should be a blind adder. The concrete pairwise-mask and PRG code lives in the child guide on implementing SecAgg masking for spatial gradients; this page is the protocol-level view that ties masking, dropout recovery, and DP composition into one round.

Secure aggregation — pairwise masks cancel in the server's modular sum; dropped clients are recovered via threshold shares Left to right: three mobile mapping clients (Client 1, Client 2, Client 3) each produce a masked update. Client 1 uploads y1 = x1 + m12 + m13 + b1, Client 2 uploads y2 = x2 − m12 + m23 + b2, Client 3 uploads y3 = x3 − m13 − m23 + b3, where each m is a pairwise mask shared by a pair of clients and each b is a per-client self-mask. Arrows carry the masked vectors to a federated server that computes S = Σ yi modulo a large integer R. Because every pairwise mask appears once as +m and once as −m across the two clients that share it, the masks cancel in the sum and the server obtains Σ yi = Σ xi — only the aggregate survives. A bottom-left panel enumerates the pairwise masks m12, m13, m23 with their sign convention and notes each appears once positive and once negative so the total is zero. A bottom-right dropout-recovery panel describes Client 3 dropping mid-round: surviving clients reveal Shamir shares of Client 3's PRG seed, the server reconstructs Client 3's key, regenerates m13 and m23, and subtracts them so the surviving-client sum still cancels, without revealing any single update. A dashed arrow feeds the recovered masks from the dropout panel back into the server as a correction. Secure aggregation — pairwise masks cancel in the sum; dropouts recovered by threshold shares clip · quantize · mask (per client) blind server: add masked updates Client 1 mobile mapping device Client 2 mobile mapping device Client 3 drops mid-round y₁ = x₁ + m₁₂ + m₁₃ + b₁ masked · mod R y₂ = x₂ − m₁₂ + m₂₃ + b₂ masked · mod R y₃ = x₃ − m₁₃ − m₂₃ + b₃ masked · mod R Federated server blind adder S = Σ yᵢ mod R masks cancel Σ yᵢ = Σ xᵢ only the aggregate survives Pairwise additive masks (Diffie–Hellman seeded) m₁₂: Client 1 adds +m₁₂, Client 2 adds −m₁₂ m₁₃: Client 1 adds +m₁₃, Client 3 adds −m₁₃ m₂₃: Client 2 adds +m₂₃, Client 3 adds −m₂₃ Each mᵢⱼ appears once as + and once as − ⇒ Σ = 0 Dropout recovery — Client 3 drops Surviving clients reveal Shamir shares of C3's PRG seed Server reconstructs C3's key → regenerates m₁₃, m₂₃ → subtracts them so the survivor sum still cancels No individual update is ever revealed to the server subtract recovered masks

Key concept. SecAgg does not encrypt gradients — it blinds them with additive noise that is engineered to sum to zero. Correctness and privacy both hinge on one algebraic fact: for every unordered pair (i,j)(i, j), one client adds +mij+m_{ij} and the other adds mij-m_{ij}, so ixi\sum_i x_i survives the sum while every mask vanishes. Everything else — Diffie–Hellman, PRGs, Shamir shares — exists only to distribute those masks without a trusted dealer and to repair the sum when a client disappears.

Prerequisites & Cryptographic Environment

The reference implementation on this page uses only the Python standard library — secrets for key material, hashlib for the PRG and key-derivation function, and the three-argument pow() for modular exponentiation and inversion. Production deployments swap the illustrative finite-field Diffie–Hellman group for X25519 (via cryptography) and add an authenticated transport, but the algebra is identical.

Four environmental invariants must hold before a round begins:

  • A shared quantization contract. Every client and the server must agree on the clipping bound CC, the quantization resolution, and the aggregation modulus RR. Masks cancel only if all parties work in the same integer ring ZR\mathbb{Z}_R; a client that quantizes to a different scale corrupts the sum silently. Choose RR large enough that the true aggregate ixi\sum_i x_i never wraps: with NN clients and per-coordinate values bounded by QQ, require R>NQR > N \cdot Q.
  • A registered participant set with authenticated public keys. Pairwise masks are seeded by Diffie–Hellman, so each client needs the authenticated public keys of its peers for the round. Without authentication a man-in-the-middle server could substitute its own keys and unmask individual clients — this is the single most important assumption in the protocol.
  • A declared dropout threshold. SecAgg tolerates clients vanishing between the masking and unmasking phases. Fix a Shamir threshold tt up front: any tt surviving clients can reconstruct a dropped client’s masks, and any t1t-1 colluding parties learn nothing. This reuses the same threshold construction described in secret sharing for coordinates, applied here to seeds rather than coordinates.
  • A privacy-budget accountant. SecAgg hides individual updates but the aggregate it releases is still a function of private data. To make the released model differentially private, updates are clipped and noised before masking; stand up a Rényi differential-privacy accountant and tie its (ε,δ)(\varepsilon, \delta) ceiling to a concrete control in the parent Differential Privacy for Geospatial Data section.

Step 1: Clip and Quantize Client Updates

SecAgg operates on integers, so the first job on each device is to turn a floating-point gradient into a bounded integer vector. Clipping the update to an 2\ell_2 norm of CC bounds each client’s sensitivity — the prerequisite for the differential-privacy noise added later — and quantization maps the clipped real values onto a fixed lattice so that modular addition is exact. A client’s spatial gradient concentrates mass on the map tiles it observed, so the clip norm CC is also the lever that bounds how much any one region can move the global model.

python
from __future__ import annotations

import numpy as np
from typing import Final

CLIP_NORM: Final[float] = 1.0          # L2 sensitivity bound per client update
Q_LEVELS: Final[int] = 1 << 20         # quantization lattice: 2^20 levels
MODULUS_R: Final[int] = 1 << 62        # aggregation ring; must exceed N * Q_LEVELS

def clip_and_quantize(update: np.ndarray, clip: float = CLIP_NORM) -> np.ndarray:
    """Clip a gradient to L2 norm `clip`, then map it onto [0, Q_LEVELS) integers.

    Clipping bounds the client's contribution (the DP sensitivity); quantization
    makes the subsequent modular masking exact. Values are biased to be
    non-negative so no two's-complement ambiguity enters the ring.
    """
    norm: float = float(np.linalg.norm(update))
    scaled: np.ndarray = update * min(1.0, clip / (norm + 1e-12))  # L2 clip
    # map [-clip, clip] -> [0, Q_LEVELS-1] as integers on a fixed lattice
    unit: np.ndarray = (scaled + clip) / (2.0 * clip)              # -> [0, 1]
    q: np.ndarray = np.round(unit * (Q_LEVELS - 1)).astype(np.int64)
    return np.clip(q, 0, Q_LEVELS - 1)

The bias into [0,Qlevels)[0, Q_{\text{levels}}) matters: masks are added modulo RR, and keeping every plaintext coordinate non-negative means the server can recover the true sum by subtracting the known bias offset (N(Qlevels1)/2N \cdot (Q_{\text{levels}}-1)/2) after aggregation rather than reasoning about signed residues. Because clipping is applied before masking, the differential-privacy sensitivity is fixed inside the client and is completely opaque to the server — see DP-SGD for geospatial models for how the clip norm and noise multiplier are jointly calibrated.

Step 2: Diffie–Hellman Key Agreement Between Clients

Every pair of clients needs a shared secret that only the two of them know and that the server can never derive on its own. Diffie–Hellman delivers exactly this without any interactive negotiation: each client publishes one public key, and any pair can independently compute the same shared secret from their own private key and the other’s public key. That shared secret becomes the seed for their pairwise mask.

python
import hashlib
import secrets
from typing import Dict, Tuple

# Illustrative finite-field DH group for a runnable harness.
# Production uses X25519 or a >= 2048-bit MODP group.
DH_PRIME: int = (1 << 256) - 189     # a 256-bit prime
DH_GEN: int = 5                      # generator of the multiplicative group

def dh_keypair() -> Tuple[int, int]:
    """Return (private, public) where public = g^private mod p."""
    priv: int = secrets.randbelow(DH_PRIME - 2) + 1
    pub: int = pow(DH_GEN, priv, DH_PRIME)
    return priv, pub

def pairwise_seed(my_priv: int, their_pub: int, salt: bytes) -> bytes:
    """Derive a 32-byte pairwise seed. Symmetric: client i and client j
    compute the SAME seed from their own private and the other's public key,
    while the server, holding neither private key, cannot."""
    shared: int = pow(their_pub, my_priv, DH_PRIME)          # g^{ab} mod p
    return hashlib.sha256(shared.to_bytes(32, "big") + salt).digest()

The symmetry is the whole point: pairwise_seed(a_priv, b_pub) equals pairwise_seed(b_priv, a_pub) because both evaluate gabmodpg^{ab} \bmod p. The server sees only the public keys, so it cannot reconstruct any seed unless it recovers a private key — which the protocol only permits, under threshold control, for clients that have already dropped out.

Step 3: Derive Pairwise Masks via a PRG

A 32-byte seed is not a mask; the mask is a full-length integer vector, one entry per gradient coordinate. A pseudorandom generator (PRG) stretches the seed deterministically into that vector, so both clients in a pair produce byte-identical masks from their shared seed without exchanging anything further. The sign convention — lower index adds, higher index subtracts — is what makes each pair’s contribution cancel in the sum.

python
def prg_expand(seed: bytes, dim: int, modulus: int) -> np.ndarray:
    """Deterministically stretch a seed into a length-`dim` mask vector in Z_modulus.

    SHA-256 in counter mode is a simple, dependency-free PRG. Both clients in a
    pair call this on the SAME seed and obtain the SAME mask; the server cannot,
    because it never holds the seed.
    """
    out: list[int] = []
    counter: int = 0
    while len(out) < dim:
        block: bytes = hashlib.sha256(seed + counter.to_bytes(8, "big")).digest()
        for k in range(0, 32, 8):
            out.append(int.from_bytes(block[k:k + 8], "big") % modulus)
        counter += 1
    return np.array(out[:dim], dtype=object)  # object dtype: exact big-int mod arithmetic

def pairwise_sign(i: int, j: int) -> int:
    """+1 for the lower-indexed client of a pair, -1 for the higher-indexed one."""
    return 1 if i < j else -1

Using object dtype keeps the arithmetic in exact Python integers modulo RR; NumPy’s fixed-width integer types would overflow silently and break cancellation. This exactness requirement is the same one that forces fixed-point integers in coordinate secret sharing — floating point or wrapping integers make the algebra non-deterministic, and a mask that does not reproduce byte-for-byte on both sides never cancels.

Step 4: Mask and Upload

Each client now assembles its masked update: the quantized gradient, plus a fresh self-mask, plus the signed sum of its pairwise masks with every other participant — all modulo RR. The self-mask bib_i is the subtle part. Without it, a client that is merely slow (its unmasking information arrives late) rather than dropped could have its pairwise masks reconstructed and its raw update exposed. The self-mask guarantees that reconstructing only the pairwise masks still leaves each individual update blinded; the server must obtain a different secret to remove the self-mask, and the protocol never hands out both for the same client.

python
class SecAggMasking:
    """Coordinator for one SecAgg round over quantized integer gradients.

    Masks are additive and cancel EXACTLY modulo R: for every pair (i, j) one
    client contributes +m_ij and the other -m_ij, and each client's self-mask
    b_i is removed only when the client survives to the unmasking phase.
    """

    def __init__(self, num_clients: int, dim: int,
                 threshold: int, modulus: int = MODULUS_R) -> None:
        if not (2 <= threshold <= num_clients):
            raise ValueError("require 2 <= threshold <= num_clients")
        self.n: int = num_clients
        self.dim: int = dim
        self.t: int = threshold
        self.R: int = modulus
        self.salt: bytes = secrets.token_bytes(16)
        # each client gets a DH keypair and an independent self-mask seed
        self._priv: Dict[int, int] = {}
        self._pub: Dict[int, int] = {}
        self._self_seed: Dict[int, bytes] = {}
        for c in range(self.n):
            self._priv[c], self._pub[c] = dh_keypair()
            self._self_seed[c] = secrets.token_bytes(32)

    def mask_update(self, client: int, quantized: np.ndarray) -> np.ndarray:
        """Return y_i = (x_i + self_mask_i + Σ_j sign(i,j)·m_ij) mod R."""
        masked: np.ndarray = quantized.astype(object) % self.R
        # self-mask: blinds the update even if pairwise masks are reconstructed
        masked = (masked + prg_expand(self._self_seed[client], self.dim, self.R)) % self.R
        for peer in range(self.n):
            if peer == client:
                continue
            seed: bytes = pairwise_seed(self._priv[client], self._pub[peer], self.salt)
            m: np.ndarray = prg_expand(seed, self.dim, self.R)
            masked = (masked + pairwise_sign(client, peer) * m) % self.R
        return masked % self.R

Two design choices carry the privacy guarantee. First, the pairwise seed is derived from the client’s own private key and the peer’s public key, so a client never transmits a mask — it derives it. Second, the self-mask seed and the DH private key are held under two different secret-sharing sets (Step 6), so the server can be granted at most one of them per client and can therefore never strip a surviving client down to its raw update.

Step 5: The Server Sums

The server’s job is deliberately trivial: it adds the masked vectors it received, modulo RR. If no client dropped, the pairwise masks cancel in pairs and the self-masks are the only residue — which the surviving clients then help remove. The server performs no cryptographic operation on individual updates; it is a blind adder, exactly the trust model that lets a semi-honest aggregator run federated rounds without becoming a single point of location leakage. This is the interface the broader async execution patterns wrap when they schedule masked uploads across flaky mobile links.

python
    def aggregate(self, masked: Dict[int, np.ndarray]) -> np.ndarray:
        """Sum masked updates modulo R. This is all the server does to the vectors."""
        acc: np.ndarray = np.zeros(self.dim, dtype=object)
        for client, y in masked.items():
            acc = (acc + y.astype(object)) % self.R
        return acc % self.R

At this point the accumulator holds ireceivedxi+ibi\sum_{i \in \text{received}} x_i + \sum_{i} b_i plus the uncancelled pairwise masks of any dropped clients. The next step removes the two mask residues so only xi\sum x_i remains.

Step 6: Recover Masks of Dropped Clients via Threshold Shares

When a client drops between masking and unmasking, its pairwise masks with the survivors no longer have a partner to cancel against, so they poison the sum. SecAgg repairs this without ever reconstructing the dropped client’s update: at round setup, each client splits two secrets with Shamir’s scheme — its DH private key and its self-mask seed — and hands one share of each to every other client. In the unmasking phase the surviving clients reveal, for each dropped client, enough shares of its private key to let the server regenerate exactly the pairwise masks that failed to cancel; and for each surviving client, enough shares of its self-mask seed to remove its self-mask. The threshold tt guarantees that fewer than tt colluding parties learn neither secret.

python
# Shamir sharing of the seed/key secrets over a large prime field.
SHAMIR_PRIME: int = (1 << 521) - 1   # Mersenne prime; holds 256-bit secrets

def shamir_split(secret: int, t: int, n: int) -> list[tuple[int, int]]:
    """Split `secret` into n shares; any t reconstruct it, any t-1 reveal nothing."""
    coeffs: list[int] = [secret] + [secrets.randbelow(SHAMIR_PRIME) for _ in range(t - 1)]
    shares: list[tuple[int, int]] = []
    for x in range(1, n + 1):
        y: int = 0
        for c in reversed(coeffs):               # Horner evaluation mod prime
            y = (y * x + c) % SHAMIR_PRIME
        shares.append((x, y))
    return shares

def shamir_recover(shares: list[tuple[int, int]]) -> int:
    """Lagrange-interpolate the secret at x = 0 from >= t shares."""
    secret: int = 0
    for i, (xi, yi) in enumerate(shares):
        num, den = 1, 1
        for j, (xj, _) in enumerate(shares):
            if i != j:
                num = (num * (0 - xj)) % SHAMIR_PRIME
                den = (den * (xi - xj)) % SHAMIR_PRIME
        secret = (secret + yi * (num * pow(den, -1, SHAMIR_PRIME)) % SHAMIR_PRIME) % SHAMIR_PRIME
    return secret

# Method on SecAggMasking: strip mask residues after aggregation.
def _unmask(self: "SecAggMasking", agg: np.ndarray,
            survivors: list[int], dropped: list[int],
            recovered_priv: Dict[int, int]) -> np.ndarray:
    """Remove survivors' self-masks and dropped clients' pairwise masks."""
    out: np.ndarray = agg.astype(object) % self.R
    # 1. remove the self-mask of every surviving client
    for s in survivors:
        out = (out - prg_expand(self._self_seed[s], self.dim, self.R)) % self.R
    # 2. cancel the orphaned pairwise masks of every dropped client
    for d in dropped:
        priv_d: int = recovered_priv[d]          # reconstructed from Shamir shares
        for s in survivors:
            seed: bytes = pairwise_seed(priv_d, self._pub[s], self.salt)
            m: np.ndarray = prg_expand(seed, self.dim, self.R)
            # survivor s carried sign(s, d)·m for this pair; undo it
            out = (out - pairwise_sign(s, d) * m) % self.R
    return out % self.R

SecAggMasking._unmask = _unmask

The reconstructed private key of a dropped client is enough to regenerate only that client’s pairwise masks — never its update, which was never included in the sum. And because the server receives the dropped clients’ private keys but only the survivors’ self-mask seeds (and vice versa is impossible within one round), it can strip the residues without ever holding both halves needed to isolate any single client. Wiring the reveal traffic through a partition-tolerant broker so a second wave of dropouts does not stall the round is the job of async execution patterns.

Step 7: Validate Exact Cancellation

The correctness invariant is unambiguous and belongs in CI: after unmasking, the server’s result must equal the plain modular sum of the surviving clients’ quantized updates — exactly, not approximately. The harness below runs a full round with no dropout and a full round with one dropout, and asserts exact equality in both.

python
def _validate() -> None:
    rng = np.random.default_rng(7)
    n, dim, t = 5, 16, 3
    sa = SecAggMasking(num_clients=n, dim=dim, threshold=t)

    # each client produces a clipped, quantized spatial gradient
    plain: Dict[int, np.ndarray] = {
        c: clip_and_quantize(rng.normal(size=dim)) for c in range(n)
    }

    # --- Round A: no dropout -------------------------------------------------
    masked = {c: sa.mask_update(c, plain[c]) for c in range(n)}
    agg = sa.aggregate(masked)
    survivors = list(range(n))
    recovered = sa.aggregate  # unused; no dropped keys to reconstruct
    result = sa._unmask(agg, survivors=survivors, dropped=[], recovered_priv={})
    expected = np.zeros(dim, dtype=object)
    for c in survivors:
        expected = (expected + plain[c].astype(object)) % sa.R
    assert list(result) == list(expected), "no-dropout sum must cancel exactly"

    # --- Round B: client 4 drops after masking -------------------------------
    dropped = [4]
    survivors = [c for c in range(n) if c not in dropped]
    masked_recv = {c: masked[c] for c in survivors}          # server never gets y_4
    agg_b = sa.aggregate(masked_recv)
    # survivors reveal >= t Shamir shares of the dropped client's DH private key
    shares_priv4 = shamir_split(sa._priv[4], t, n)[:t]
    recovered_priv = {4: shamir_recover(shares_priv4)}
    assert recovered_priv[4] == sa._priv[4], "Shamir must reconstruct the exact key"
    result_b = sa._unmask(agg_b, survivors=survivors,
                          dropped=dropped, recovered_priv=recovered_priv)
    expected_b = np.zeros(dim, dtype=object)
    for c in survivors:
        expected_b = (expected_b + plain[c].astype(object)) % sa.R
    assert list(result_b) == list(expected_b), "survivor sum must cancel after dropout"

    print("all SecAgg cancellation invariants hold")

if __name__ == "__main__":
    _validate()

The two assertions are the security-relevant ones: the unmasked output is identical to the direct sum of plaintext updates, proving the masks contributed nothing to the aggregate, and the Shamir reconstruction reproduces the dropped client’s key exactly, proving dropout recovery is lossless. Any deviation — a wrong modulus on one client, a NumPy integer overflow, a mismatched salt — turns these exact-equality checks red.

Threat Model Considerations

Secure aggregation changes the server’s job from “read a gradient” to “collude past a threshold,” but the geospatial setting adds specific edges:

  • Malicious key substitution. A server that forges or swaps public keys during setup can seed pairwise masks it controls, then unmask a target client. This is why authenticated public-key distribution is a hard prerequisite, not an optimization — an unauthenticated SecAgg deployment offers no protection against an active server.
  • Self-mask omission. Dropping the self-mask to save bandwidth reopens the “slow-not-dropped” attack: the server declares a merely-late client dropped, reconstructs its pairwise masks, and reads its raw spatial gradient. Always include the self-mask and share its seed under a threshold distinct from the private key’s.
  • Aggregate-level leakage. SecAgg hides individual updates but the released sum is still sensitive; with a small cohort or a lone contributor from a sparse region, the aggregate approximates one client’s gradient. Compose with differential privacy — clip and noise before masking — and enforce a minimum cohort size, informed by handling non-IID geospatial data in federated learning where regional skew makes single-client dominance more likely.
  • Collusion at or above threshold. If the server colludes with t1t-1 clients it can reconstruct a victim’s masks. Set tt from real trust boundaries — distinct device operators, distinct network segments — not a convenient fraction of NN.
  • Modulus overflow. If RNQlevelsR \le N \cdot Q_{\text{levels}} the true aggregate wraps and the recovered sum is silently wrong. Size RR against the worst-case cohort and per-coordinate bound, with headroom.
  • Replay across rounds. A pairwise mask reused across rounds lets a server difference two masked updates to cancel the mask and expose an update delta. Derive the seed salt fresh per round so masks never repeat.

Validation & Compliance Checklist

Wire each control into CI and a compliance dashboard with a measurable pass criterion:

  1. Exact cancellation — PASS if the unmasked aggregate equals the modular sum of plaintext quantized updates for 100% of a randomized property test, with and without simulated dropout. This is the core correctness invariant; fail the build on any mismatch.
  2. Dropout tolerance — PASS if the round completes correctly when any NtN - t clients drop after masking, and fails cleanly (raised error, never a wrong sum) when more than NtN - t drop.
  3. Threshold secrecy — PASS if any t1t - 1 share subset provably fails to reconstruct a seed or private key, mirroring the boundary test in secret sharing for coordinates.
  4. Modulus headroom — PASS if R>Nmax(Qlevels1)R > N_{\max} \cdot (Q_{\text{levels}} - 1) for the largest configured cohort, asserted at startup so no round can wrap.
  5. DP composition — PASS if every client update is clipped to CC and noised to the calibrated σ\sigma before masking, and the composed spend stays under the (ε,δ)(\varepsilon, \delta) ceiling declared in the Differential Privacy for Geospatial Data mapping.
  6. Per-round freshness — PASS if the seed salt and self-mask seeds are regenerated each round, verified by asserting no mask vector repeats across two consecutive rounds.

The _validate() harness in Step 7 exercises invariants 1 and 2 directly and is safe to run in CI; extend it with a t1t-1 negative case to cover invariant 3.

Failure Modes & Remediation

Secure aggregation fails quietly — a wrong modulus returns a plausible-but-wrong sum rather than an error. The high-frequency production breakages:

  • Cascading dropout below threshold. A second wave of clients drops during the unmasking phase, leaving fewer than tt survivors to reconstruct a dropped client’s key. Detection: fewer than tt reveal messages at the round timeout. Recovery: abort the round and retry with a larger cohort, or raise NN relative to tt to widen the survival margin; never accept a partially-unmasked sum.
  • Modulus mismatch across clients. One client ships a build with a different RR or quantization scale; its masked update does not cancel and the whole aggregate is corrupt. Detection: a checksum of the quantization contract mismatches at registration. Recovery: pin the contract in signed round metadata and reject clients whose checksum differs.
  • NumPy overflow. A mask vector held in int64 instead of Python object dtype overflows, breaking cancellation on long gradients. Detection: the exact-equality validation fails on high-dimensional updates. Recovery: keep all ring arithmetic in exact big integers.
  • Stale-mask replay. A round reuses the previous salt, so an attacker differences two rounds to cancel the mask. Detection: identical mask vectors observed across rounds. Recovery: derive the salt from a fresh per-round nonce and rotate self-mask seeds.
  • DP budget exhaustion. Repeated rounds over the same device cohort deplete the (ε,δ)(\varepsilon, \delta) budget, after which the aggregated model no longer carries the claimed guarantee even though masking still works. Detection: the RDP accountant crosses its ceiling. Recovery: halt training for that cohort, coarsen the noise multiplier, or rotate to fresh clients.

Frequently Asked Questions

How is SecAgg different from just encrypting each gradient?

Encryption would require the server to decrypt something to aggregate, reintroducing a point where an individual update is visible. SecAgg never decrypts — it adds masked integers, and the masks are constructed to cancel in the sum. The server learns the aggregate directly from masked inputs and holds no key that could isolate one client, which is a strictly stronger trust model for a semi-honest aggregator.

Why are both a self-mask and pairwise masks needed?

Pairwise masks cancel between clients and protect against a passive server, but they can be reconstructed for a dropped client. Without a self-mask, a server could falsely declare a merely-slow client dropped, reconstruct its pairwise masks, and read its raw update. The self-mask blinds each update independently, and because its seed is shared under a different threshold set than the private key, the server can obtain at most one of the two secrets per client and never both.

What happens to correctness when a mobile client drops mid-round?

Its pairwise masks with the surviving clients lose their cancellation partner and would corrupt the sum. The survivors reveal enough Shamir shares of the dropped client's Diffie–Hellman private key for the server to regenerate exactly those orphaned masks and subtract them. The dropped client's update was never in the sum, so recovery removes masks without ever exposing an update, and the survivor aggregate cancels exactly.

Does SecAgg on its own make the trained model differentially private?

No. SecAgg hides individual updates but the released aggregate is still a function of private gradients, and with a small or skewed cohort the sum can approximate one client. Differential privacy is layered on by clipping each update to a fixed L2 norm and adding calibrated Gaussian noise before masking; masking is transparent to the sum, so the DP guarantee on the clipped-and-noised updates carries through to the aggregate. The clip norm doubles as the SecAgg sensitivity bound.

How large must the aggregation modulus be?

Large enough that the true aggregate never wraps: with N clients and each quantized coordinate bounded by Q levels, the modulus R must exceed N times Q. If R is too small the sum wraps around the ring and the recovered aggregate is silently wrong, so the bound is asserted at startup against the largest configured cohort with headroom to spare.

This guide is part of the Federated Learning Workflows for Geospatial Data reference — start there for how client selection, aggregation, synchronization, and secure aggregation fit into one training loop.

Up: Federated Learning Workflows for Geospatial Data