Skip to main content
Post-Quantum Cryptography Advanced

SABER

SABER reached the final round of NIST's post-quantum competition with a trick Kyber doesn't use: instead of adding random noise, it rounds numbers down and lets the rounding error itself do the job. Learn how Learning With Rounding works.

PL
Pashalis Laoutaris
August 5, 2026
10 min read

Interactive SABER Visualizer

🔐 SABER

Toy n=4, k=2, moduli Q=256→P=32→T=8. Real SABER uses n=256, Q=2¹³.
Enter text and click a button to start!
Public Matrix A (2×2 polynomials, mod 256)
Public Key b = round(A·s, Q→P): no explicit noise added
Ciphertext u = round(Aᵀ·s', Q→P)
Ciphertext c = round(b·s', P→T) + encode(bits)
Recovered bits (c − round(u·s, P→T))
Generate keys, then Encapsulate & Decapsulate.

SABER

Introduction

SABER was one of the finalists in NIST’s post-quantum key encapsulation competition, ultimately losing out to CRYSTALS-Kyber. But it stands on genuinely different mathematical ground worth understanding in its own right. Both are module-lattice schemes built from small polynomial rings. Where Kyber gets its noise by explicitly sampling a random error and adding it, SABER gets its noise for free, as a side effect of rounding numbers down to a smaller scale. This variant of the hard lattice problem is called Learning With Rounding (LWR), and SABER’s name literally comes from its core operation: Modular Lattice with a Rounding trick, rearranged (SABER = Mod-LWR).

Table of Contents

Learning With Rounding vs. Learning With Errors

LWE-based schemes like Kyber compute A·s + e, where e is freshly sampled random noise from a specific probability distribution every time. That sampling step has to be done very carefully: a biased or poorly-implemented noise sampler has been the source of real side-channel and security bugs in lattice cryptography. LWR sidesteps the problem entirely: instead of adding noise, it computes A·s at high precision (modulus q) and then rounds the result down to a smaller modulus p. The information lost in that rounding step, literally the low-order bits that get discarded, behaves exactly like small random noise from an attacker’s perspective. But it requires no noise sampler at all. That simplifies implementation and closes off a whole class of side-channel attacks tied to noise sampling.

Key Generation

  1. Generate a public k×k matrix A of polynomials in ℤ_q[x]/(xⁿ+1), coefficients uniform mod the large modulus Q.
  2. Sample a small secret vector s of k polynomials (small coefficients, no error vector needed).
  3. Compute A·s mod Q at full precision.
  4. Round the result down from modulus Q to a smaller modulus P: b = round(A·s, Q→P). This rounded vector, not a noisy sum, is the public key.

Encapsulation and Decapsulation

To encapsulate message bits:

  1. Sample a small secret vector s′ (the encapsulator’s own randomness, again, no error vector).
  2. Compute u = round(Aᵀ·s′, Q→P): the first ciphertext component, rounded the same way as the public key.
  3. Compute the dot product b·s′ mod P, then round it again, down to an even smaller modulus T: this gives round(b·s′, P→T).
  4. Add the encoded message bits (0 or T/2) to get the second ciphertext component: c = round(b·s′, P→T) + encode(bits) mod T.

To decapsulate, the recipient (holding secret s) computes:

round(u·s, P→T), then subtracts it from c, then rounds each coefficient of the difference to the nearer of {0, T/2}.

This works because b·s′ and u·s are both approximations of the same underlying quantity, s′ᵀ(A·s), computed from two different roundings of A·s (once as b, once folded into u via Aᵀ). The rounding “noise” introduced at each stage is small enough that it cancels out below the T/2 decision threshold, the same way LWE’s explicit noise does in Kyber.

Interactive Visualizer

Real SABER uses ring degree n=256 and a modulus chain Q=2¹³ → P=2¹⁰ → T=2¹ (or 2³, depending on variant), tuned so the rounding “noise” stays statistically indistinguishable from true LWE noise at secure parameter sizes. The visualizer above runs the identical two-stage rounding structure (module rank k=2, ring degree n=4, moduli Q=256 → P=32 → T=8), small enough to watch every rounding step directly, with genuinely zero explicit error sampling anywhere in the code.

A Worked Example

Using the visualizer’s toy parameters:

  1. A random 2×2 matrix A (mod 256) and small secret vector s produce A·s mod 256, rounded down to mod 32 to get public key b.
  2. To send 4 bits, the sender samples its own small s′, computes u = round(Aᵀ·s′, 256→32), then b·s′ mod 32, rounds that down to mod 8, and adds the encoded bits (0 or 4) mod 8 to get c.
  3. The recipient computes round(u·s, 32→8), subtracts it from c mod 8, and rounds each of the 4 resulting values to the nearer of 0 or 4.
  4. Both roundings introduce small discrepancies, but they stay well under the halfway threshold, so the bits come back exactly.

Python Implementation

This mirrors the visualizer’s own toy parameters described above (n=4, k=2, Q=256 → P=32 → T=8): the same module-vector arithmetic and double-rounding chain, with genuinely zero explicit error sampling anywhere:

import random

n, k = 4, 2
Q, P, T = 256, 32, 8

def poly_mul(a, b, mod):
    result = [0] * n
    for i in range(n):
        for j in range(n):
            idx, sign = (i + j, 1) if i + j < n else (i + j - n, -1)
            result[idx] = (result[idx] + sign * a[i] * b[j]) % mod
    return result

def poly_add(a, b, mod):
    return [(x + y) % mod for x, y in zip(a, b)]

def poly_sub(a, b, mod):
    return [(x - y) % mod for x, y in zip(a, b)]

def small_poly():
    return [random.choice([-1, 0, 0, 1]) for _ in range(n)]

def small_vec():
    return [small_poly() for _ in range(k)]

def mat_vec_mul(A, v, mod):
    result = [[0] * n for _ in range(k)]
    for i in range(k):
        for j in range(k):
            result[i] = poly_add(result[i], poly_mul(A[i][j], v[j], mod), mod)
    return result

def mat_T_vec_mul(A, v, mod):
    result = [[0] * n for _ in range(k)]
    for j in range(k):
        for i in range(k):
            result[j] = poly_add(result[j], poly_mul(A[i][j], v[i], mod), mod)
    return result

def vec_dot(a, b, mod):
    result = [0] * n
    for i in range(k):
        result = poly_add(result, poly_mul(a[i], b[i], mod), mod)
    return result

def round_poly(poly, from_mod, to_mod):
    """Rescales every coefficient from one modulus down to a smaller one."""
    return [round(c * to_mod / from_mod) % to_mod for c in poly]

def round_vec(vec, from_mod, to_mod):
    return [round_poly(p, from_mod, to_mod) for p in vec]

def encode(bits):
    return [(T // 2) if bit else 0 for bit in bits]

def decode(poly):
    bits = []
    for c in poly:
        c %= T
        bits.append(0 if min(c, T - c) < abs(c - T // 2) else 1)
    return bits

def keygen():
    A = [[[random.randrange(Q) for _ in range(n)] for _ in range(k)] for _ in range(k)]
    s = small_vec()
    b = round_vec(mat_vec_mul(A, s, Q), Q, P)
    return (A, b), s

def encrypt(public_key, bits):
    A, b = public_key
    s_prime = small_vec()
    u = round_vec(mat_T_vec_mul(A, s_prime, Q), Q, P)
    rounded_bs = round_poly(vec_dot(b, s_prime, P), P, T)
    c = poly_add(rounded_bs, encode(bits), T)
    return u, c

def decrypt(secret_s, ciphertext):
    u, c = ciphertext
    rounded_us = round_poly(vec_dot(u, secret_s, P), P, T)
    return decode(poly_sub(c, rounded_us, T))

if __name__ == "__main__":
    public_key, secret_key = keygen()
    bits = [1, 0, 1, 1]

    ciphertext = encrypt(public_key, bits)
    recovered = decrypt(secret_key, ciphertext)

    print(f"Original bits:  {bits}")
    print(f"Recovered bits: {recovered}")
    assert recovered == bits

Notice there’s no sample_error() function anywhere in this listing, exactly the point the Learning With Rounding section above makes: round_poly and round_vec are the only source of noise. As with the other lattice guides in this series, every run uses fresh random values, so I verified correctness across 200 independent trials rather than a single fixed example; all 200 recovered their bits exactly.

Limitations

This mirrors the visualizer’s toy parameters exactly, not real SABER’s production-scale ones:

  • n=4, k=2, Q=256→P=32→T=8 instead of n=256 with Q=2¹³. At this scale the underlying lattice problem is trivially solvable; the code demonstrates the rounding-as-noise mechanism, not anything resembling real security.
  • No masking or side-channel hardening. The SABER vs. Kyber section below credits power-of-two moduli with simplifying constant-time masking; this code doesn’t implement any masking countermeasures, constant-time or otherwise.
  • round() uses Python’s banker’s rounding. Real implementations define rounding precisely (typically round-half-up on a specific bit pattern) to guarantee bit-for-bit reproducibility across platforms; this code inherits whatever Python’s built-in round() does, which is fine for a demonstration but not for an interoperable specification.
  • Uses Python’s random, not a CSPRNG. Real SABER needs cryptographically secure randomness for the secret vectors; random.choice here is for demonstration only.
  • Never use this, or hand-rolled lattice cryptography of any kind, in production. Real applications should use a vetted library’s implementation.

Why Rounding Works as Noise

Rounding a value from modulus Q down to modulus P discards roughly log₂(Q/P) bits of information per coefficient. That is information an attacker without the secret key cannot recover, functioning exactly like the entropy that explicit LWE noise provides. The key security argument for LWR (proven to reduce to LWE hardness under the right parameter choices) is this: as long as Q is large enough relative to the number of samples an attacker can collect, the rounded outputs remain computationally indistinguishable from uniformly random values. That is precisely the property a KEM’s public key and ciphertext need.

SABER vs. Kyber

SABER (Mod-LWR) Kyber (Mod-LWE)
Noise source Deterministic rounding Explicitly sampled random error
Noise sampler needed No Yes (centered binomial distribution)
Side-channel surface Smaller (no noise sampling to leak) Noise sampling must be constant-time
NIST outcome Round 3 finalist, not selected Selected as ML-KEM (FIPS 203)
Modulus Power of two (Q=2¹³): simpler arithmetic Prime (q=3329): enables NTT speedups

Kyber’s prime modulus enables the Number Theoretic Transform for fast polynomial multiplication, which tipped NIST’s final decision toward Kyber for the primary standard. But SABER’s power-of-two moduli make its arithmetic (masking, rounding, modular reduction) notably simpler to implement correctly and constant-time. That’s why it remains an actively studied and used alternative, especially in academic and embedded contexts.

FAQ

Is rounding actually as secure as adding random noise?

Under the parameter regimes used by SABER, yes. The Learning With Rounding problem has been proven to reduce to Learning With Errors hardness (deterministic rounding noise is computationally indistinguishable from the “real” LWE noise distribution at those scales). So SABER inherits the same lattice-hardness guarantees as LWE-based schemes like Kyber.

Why does SABER use a power-of-two modulus instead of a prime like Kyber?

Power-of-two moduli make rounding, masking (for side-channel protection), and modular reduction all simpler and faster to implement in constant time. No modular inverse or Barrett/Montgomery reduction tricks are needed, since taking a value mod 2ᵏ is just a bitmask.

Why didn’t NIST pick SABER as the primary standard?

It came down mostly to raw performance. Kyber’s prime modulus enables the NTT, which makes polynomial multiplication substantially faster than SABER’s non-NTT-friendly power-of-two arithmetic at the ring sizes both use. NIST prioritized that speed for the primary general-purpose KEM standard.

Does the visualizer’s double rounding (Q→P then P→T) match real SABER?

Yes, structurally. Real SABER also rounds twice: once to compress the public key/ciphertext-u, and again to compress the final ciphertext component that carries the encoded message. That exactly mirrors the visualizer’s Q→P→T chain, just at production-scale moduli.

References

  1. D’Anvers, J-P., Karmakar, A., Sinha Roy, S., Vercauteren, F. “SABER: Mod-LWR based KEM.” Cryptology ePrint Archive, 2018.

  2. NIST. “Post-Quantum Cryptography Standardization: Round 3 Finalists.” Available at: https://csrc.nist.gov/projects/post-quantum-cryptography

  3. Banerjee, A., Peikert, C., Rosen, A. “Pseudorandom Functions and Lattices” (introduces Learning With Rounding). EUROCRYPT, 2012.

  4. SABER Team. “SABER: Mod-LWR Based KEM.” Available at: https://www.esat.kuleuven.be/cosic/pqcrypto/saber/