Chapter 2: Mathematical preliminaries
The algebra-heavy parts of post-quantum cryptography in this book repeatedly use four kinds of algebraic object: integers modulo , finite fields, polynomial quotient rings, and vectors or matrices over those rings. Lattice schemes (Part II), code-based schemes (Part IV), and the post-quantum zero-knowledge layer (Part VI) all reach into this toolbox. Hash-based signatures (Part III) do not. They are built on hash functions and Merkle trees, and Chapter 14 introduces the small set of primitives they need on its own terms. Chapter 2 defines each algebraic object, states the one or two theorems the later chapters cite, and implements the basic operations in Python. Readers who want to verify they have the assumed background should see Prerequisites.
Every code block in this chapter is self-contained and runnable in isolation: the chapter’s block verifier runs each block in a fresh Python namespace, so every block that depends on an earlier helper restates that helper at the top. A reader copying a single block into a file gets a script that runs without any external imports. The trailing # ==> marker is the expected-output assertion checked by tools/verify_code_blocks.py. There is no error handling on any helper. A helper given bad input crashes loudly, which is the intended behavior for a toy implementation. Asserts are used as narration of preconditions, not as error recovery.
A calculation that reappears
Section titled “A calculation that reappears”One kind of calculation dominates what later chapters do with integers: raise a base to a large power and reduce the result modulo another integer. Here is the smallest instance that shows up. Take the prime 13 and the base 7, and compute .
Python’s built-in pow does this in time logarithmic in the exponent by repeated squaring:
direct = pow(7, 200, 13)print(direct)# ==> 3Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch02/, one file per block. Appendix C covers the clone and the environment they run on.
Fermat’s little theorem gives a shortcut (Shoup, 2009). For any prime and any integer not divisible by , . For and , that means . Since , the exponent collapses modulo 12 to 8, so :
by_fermat = pow(7, 8, 13)print(by_fermat)# ==> 3Both routes return the same element of , because they are computing the same thing by different paths. Everything in the rest of the chapter is machinery for doing this kind of calculation carefully in richer algebraic settings. Integers modulo are the first setting. Finite fields, polynomial quotient rings, and matrices over those rings are the rest of the catalog.
Integers modulo n
Section titled “Integers modulo n”Fix a positive integer . Two integers and are congruent modulo , written , when divides . The set of equivalence classes under congruence is , the integers modulo . Addition and multiplication pass to equivalence classes without modification, so is a commutative ring with identity (Shoup, 2009).
An element of is a unit (has a multiplicative inverse) exactly when . The extended Euclidean algorithm computes together with integers and satisfying . When , the coefficient reduced modulo is the inverse of (Shoup, 2009). The code below implements this.
Two theorems about powers come up in the number-theoretic chapters. Fermat’s little theorem says that for a prime and an integer not divisible by , (Shoup, 2009). Euler’s generalization says that for and any with , (Shoup, 2009). The function is the number of integers with and . For a prime the count equals , and Euler collapses to Fermat.
The Chinese remainder theorem handles multiple moduli at once. Suppose are pairwise coprime, and let be their product. The map that sends to the tuple is a ring isomorphism (Shoup, 2009). Concretely, arithmetic in becomes arithmetic in each factor in parallel, and the reverse map reassembles a result in from its images in the factors.
Fast exponentiation (repeated squaring) computes in modular multiplications by walking the binary representation of the exponent from low bit to high bit (Shoup, 2009). Chapter 4 uses it to build toy RSA, and Chapter 9 uses it to raise a primitive root of unity to the powers the number theoretic transform needs.
Modular exponentiation
Section titled “Modular exponentiation”Raise a base to a non-negative exponent modulo by repeated squaring. The loop walks the exponent from low bit to high bit, squaring the running base each step and multiplying it into the result whenever the current bit is 1.
def mod_pow(base, exponent, modulus): assert exponent >= 0, "exponent must be non-negative" assert modulus >= 1, "modulus must be at least 1" result = 1 % modulus base = base % modulus while exponent > 0: if exponent % 2 == 1: result = (result * base) % modulus base = (base * base) % modulus exponent //= 2 return result
# 7^200 mod 13, matching the opening calculation.print(mod_pow(7, 200, 13))# ==> 3Python’s built-in pow(base, exponent, modulus) does the same computation with a more careful C implementation, and the later chapters use it wherever speed matters. The explicit version here exists to show what the built-in is doing inside.
The extended Euclidean algorithm
Section titled “The extended Euclidean algorithm”Given non-negative integers and , find their greatest common divisor together with coefficients and satisfying . When the gcd is and the trivial coefficients work; otherwise, solve recursively on and repackage the coefficients by the usual substitution. Non-negativity is a real precondition, not a formality: on a negative argument the recursion can bottom out on a negative value and hand back a negative . mod_inv is unaffected, because it reduces its argument modulo before calling.
def ext_gcd(a, b): if b == 0: return a, 1, 0 g, x1, y1 = ext_gcd(b, a % b) return g, y1, x1 - (a // b) * y1
def mod_inv(a, modulus): assert modulus > 1, "modulus must be greater than 1" g, x, _ = ext_gcd(a % modulus, modulus) assert g == 1, "inverse does not exist: a and modulus share a factor" return x % modulus
# Compute 17^-1 modulo 43 and verify the inverse.inverse = mod_inv(17, 43)product = (17 * inverse) % 43print(inverse)print(product)# ==> 38# ==> 1The assert is narration, not error handling. It marks a precondition for the toy reader. A production implementation would raise a specific exception, but a toy helper that quietly returns garbage on invalid input teaches the wrong lesson.
Finite fields
Section titled “Finite fields”When is a prime , every nonzero element of has a multiplicative inverse, so is a field. Write it when treating it as a field, and for the set of its nonzero elements under multiplication.
is a cyclic group of order (Lidl & Niederreiter, 1997). Cyclic means there is an element with the property that every nonzero element of equals for some integer in . Such a is called a primitive element or a generator of . For the concrete primes this book uses, a generator is found by trial, as Exercise 2 walks through for .
The prime fields are the only finite fields Part II builds from scratch. Chapter 11’s partial number theoretic transform computes inside quadratic extensions of , which that chapter introduces where it needs them. Finite fields of the form also exist and appear in the Classic McEliece treatment in Chapter 20. They are constructed as for an irreducible polynomial of degree , which is an instance of the polynomial-quotient construction in the next subsection (Lidl & Niederreiter, 1997). Chapter 20 writes this field , where is the extension degree and the field has elements. The Goppa-code dimension parameter is a separate quantity and lives at the code level, not at the field level.
Polynomial rings and quotient rings
Section titled “Polynomial rings and quotient rings”Fix a prime . The polynomial ring is the set of polynomials in one variable with coefficients in , under the usual addition and multiplication of polynomials. is a Euclidean domain: for any in with nonzero there exist unique in with and (Lang, 2002; Shoup, 2009). The zero polynomial has degree by convention, which is what lets a zero remainder satisfy that bound. Finding and is polynomial long division, which the code below implements.
When is prime, the symbols , , and all refer to the same field. The book follows the convention used in NIST’s lattice specifications and writes when the modulus is the integer used by a scheme parameter set, and when the prime is generic.
Given a polynomial in of degree , the quotient ring consists of equivalence classes of polynomials modulo . A canonical representative of each class is the polynomial of degree strictly less than obtained by taking the remainder on long division by . Addition and multiplication in the quotient are performed in and then reduced modulo (Lang, 2002).
When is irreducible over , every nonzero element of has a multiplicative inverse, and the quotient is a field with elements (Lidl & Niederreiter, 1997). This is how the finite fields are built. When is nonconstant and reducible, the quotient is still a commutative ring with identity, but it is not a field and it has zero divisors. It is still usable for arithmetic.
The lattice-based schemes in Part II work in quotient rings of the form , where is a prime and is a power of two. With a power of two, is the cyclotomic polynomial , and the quotient is the negacyclic ring of polynomials of degree less than in which is identified with . The parameter is then chosen so that multiplication in this ring admits an efficient number theoretic transform.
ML-KEM uses and (National Institute of Standards and Technology, 2024). The prime 3329 satisfies . So contains a primitive -th root of unity but not a primitive -th root. The polynomial factors over into 128 degree-two irreducible polynomials rather than 256 linear factors (FIPS 203 §4.3, equation 4.10) (National Institute of Standards and Technology, 2024). Chapter 9 derives the condition behind this partial split, and Chapter 11 builds the corresponding transform.
For Chapter 2 it is enough to know that is a ring, that its elements are polynomials of bounded degree, and that multiplication is ordinary polynomial multiplication followed by reduction modulo . Figure 2.1 shows that reduction in the negacyclic case at .
Polynomials over a finite field
Section titled “Polynomials over a finite field”Represent a polynomial as a list of coefficients in ascending order, so the list [2, 1, 3] stands for . Multiplication is the textbook double loop. The result has degree at most , and every entry is reduced modulo as it is accumulated. Chapter 9 replaces this with the number theoretic transform for the specific rings ML-KEM uses, but the naive version here is what the rest of Part II’s exposition assumes the reader has in mind.
def poly_mul(f, g, p): result = [0] * (len(f) + len(g) - 1) for i, a in enumerate(f): for j, b in enumerate(g): result[i + j] = (result[i + j] + a * b) % p return result
# Multiply (2 + x) by (3 + x^2) in F_5[x].f = [2, 1]g = [3, 0, 1]print(poly_mul(f, g, 5))# ==> [1, 3, 2, 1]The constant term is . The coefficient is . The coefficient is . The coefficient is . Reading the list back as a polynomial gives in .
Polynomial reduction
Section titled “Polynomial reduction”Long division of a polynomial by a monic polynomial over produces a remainder with . Repeatedly peel off the top coefficient of , subtract the appropriate multiple of from the tail, and drop the now-zero top coefficient. This helper assumes is monic for simplicity; a production helper would handle a general leading coefficient by dividing it out first. The first line reduces every incoming coefficient modulo , so a caller that passes unreduced integers gets coefficients back in . That is not the same as a canonical representative, because the trailing zero coefficients are still left in place.
def poly_mul(f, g, p): result = [0] * (len(f) + len(g) - 1) for i, a in enumerate(f): for j, b in enumerate(g): result[i + j] = (result[i + j] + a * b) % p return result
def poly_mod(a, f, p): assert f[-1] == 1, "poly_mod requires f to be monic" a = [c % p for c in a] deg_f = len(f) - 1 while len(a) - 1 >= deg_f: lead = a[-1] if lead != 0: for i in range(deg_f + 1): a[-1 - i] = (a[-1 - i] - lead * f[deg_f - i]) % p a.pop() return a
# In F_7[x], reduce (1 + x + x^3) * (2 + x^2) modulo x^3 + x + 1.# Any multiple of the modulus reduces to the zero polynomial.f_mod = [1, 1, 0, 1]g = [2, 0, 1]product = poly_mul(f_mod, g, 7)print(poly_mod(product, f_mod, 7))# ==> [0, 0, 0]The modulus is a factor of the product by construction, so the remainder is the zero polynomial. poly_mod does not trim trailing zero coefficients, so the zero polynomial comes back as [0, 0, 0] rather than the shorter [0] or []. A caller that wants the shortest representation has to trim the result itself.
Vectors, matrices, and bases
Section titled “Vectors, matrices, and bases”A vector in is an ordered tuple of elements from . An -by- matrix over is a rectangular array of elements of with rows and columns. Addition is componentwise; scalar multiplication scales every entry by a fixed element of ; matrix multiplication is the usual row-times-column rule. These operations make a vector space over (Dummit & Foote, 2004).
Two ideas from linear algebra carry into the later chapters. The first is rank. The rank of a matrix is the dimension of its row space. Equivalently, it is the dimension of its column space. The two dimensions are equal, even though the row and column spaces live in different ambient vector spaces when is rectangular. Gaussian elimination by row operations reduces to reduced row echelon form without changing its rank. The rank is the number of nonzero rows in the reduced form (Dummit & Foote, 2004). The code below implements this algorithm.
The second is a basis. A basis of is a set of vectors that spans and is linearly independent. Over a field, every spanning set contains a basis and every linearly independent set extends to a basis (Dummit & Foote, 2004). Over , which is not a field, the word “basis” has a narrower meaning tied to the structure of integer lattices. Chapter 7 develops that narrower meaning in full; for this chapter, “basis over ” is the only version in scope.
The standard dot product over defines a symmetric bilinear form, which coding theory routinely calls an inner product, and it is used algorithmically (for example, in the syndrome calculation of a linear code). It is not positive definite: finite fields carry no compatible ordering, and a nonzero vector can be orthogonal to itself. Over , the nonzero vector has self-inner-product . What does carry over is the part coding theory needs. Orthogonality and orthogonal complements remain well defined, which is what a parity-check matrix relies on. Chapter 19 returns to this form over when setting up linear codes.
Gaussian elimination over a finite field
Section titled “Gaussian elimination over a finite field”Reduce a matrix over to reduced row echelon form by processing one column at a time. The rank of the matrix is the number of pivots the algorithm places. Over a field this is straightforward because every nonzero scalar is invertible. Over a ring that is not a field, the algorithm breaks in informative ways. The lattice and code-based chapters each return to row reduction in a more delicate form. Inside the pivot-normalization step this block uses pow(a, p - 2, p) as the modular inverse, because the modulus is a prime and Fermat’s little theorem gives the inverse in one line. This is equivalent to the extended-Euclidean mod_inv from the earlier block. The Fermat form is just shorter when the modulus is known to be prime.
def gauss_eliminate(matrix, p): assert p > 1, "p must be prime; this helper does not test primality" m = [row[:] for row in matrix] rows = len(m) cols = len(m[0]) if m else 0 rank = 0 for col in range(cols): pivot = None for r in range(rank, rows): if m[r][col] % p != 0: pivot = r break if pivot is None: continue m[rank], m[pivot] = m[pivot], m[rank] # Fermat: for prime p, the inverse of a nonzero element is a^(p-2). inv = pow(m[rank][col], p - 2, p) m[rank] = [(x * inv) % p for x in m[rank]] for r in range(rows): if r != rank and m[r][col] % p != 0: factor = m[r][col] m[r] = [(m[r][c] - factor * m[rank][c]) % p for c in range(cols)] rank += 1 return m, rank
matrix = [ [1, 2, 3], [0, 1, 4], [2, 0, 0],]reduced, rank = gauss_eliminate(matrix, 5)for row in reduced: print(row)print("rank =", rank)# ==> [1, 0, 0]# ==> [0, 1, 4]# ==> [0, 0, 0]# ==> rank = 2The third row of the input is twice the first row plus the second row, all in : . So the three rows span a two-dimensional subspace, the algorithm correctly reports rank 2, and the third row reduces to zero.
When to reach for which tool
Section titled “When to reach for which tool”Each of the four algebraic objects in this chapter has a home in a later part of the book.
| Object | Setting for | Chapters |
|---|---|---|
| Integers modulo | Classical RSA and classical Diffie-Hellman | Chapter 4 (toy RSA) and Chapter 5 (toy Diffie-Hellman), both of them the foil for the post-quantum chapters that follow |
| Finite fields and polynomial quotient rings | The lattice-based schemes: the ring that ML-KEM works in, where the number theoretic transform gives a fast multiplication algorithm | Chapter 9 |
| Matrices over | The code-based schemes: linear codes as subspaces of , binary Goppa codes over the extension field in Classic McEliece, and HQC, whose security rests on quasi-cyclic syndrome decoding, a structured variant of the same decoding problem | Chapter 19 (linear codes), Chapter 20 (Classic McEliece), Chapter 21 (HQC) |
| Linear algebra over | The lattice chapters: lattices as the integer combinations of a basis of , then basis reduction | Chapter 7 (building lattices), Chapter 13 (basis reduction) |
Hash-based signatures in Part III sit outside this map entirely: they need hash functions and Merkle trees, not rings or matrices.
Where Chapter 2 ends and Chapter 3 picks up
Section titled “Where Chapter 2 ends and Chapter 3 picks up”This chapter answered “what is the object and how do I compute in it”. It said nothing about why anyone would build a cryptosystem on one. Chapter 3 asks the other question: given these objects, which computational problems over them are believed to be hard, and hard for a quantum adversary in particular.
The map from this chapter to that one is partial. Two of Chapter 3’s four families rest directly on the algebra above: short and close vectors in lattices, and syndrome decoding of a random linear code. A third rests on isogeny and endomorphism-ring problems over supersingular elliptic curves, which are not built from anything defined here. Chapter 3 defines the vocabulary it needs on the spot and leaves the full structure to Chapter 22. The fourth needs none of this chapter’s algebra at all, resting instead on the preimage and collision properties of hash functions.
Exercises
Section titled “Exercises”-
Use the chapter’s
mod_powhelper to compute . Verify the answer by computing (Fermat’s little theorem says , and , so the exponent reduces to 8 modulo 12). Confirm the two answers agree and state what the common answer is. -
Find a generator of . Try first: compute modulo 17 and check whether the sequence hits every nonzero element of exactly once. If does not work, try , then , and so on. Write out the full power sequence for the first generator you find.
-
Write a Python function that returns every root a polynomial in has in , by evaluating it at every element of in turn. Apply the function to over . List the values modulo 7. A polynomial of degree 2 or 3 over a field is irreducible if and only if it has no root in the field. (Any proper factorization of such a polynomial would include a linear factor, and linear factors correspond to roots.) State whether is irreducible and justify the answer from the values. The equivalence does not extend to degree 4 and higher: over is reducible but has no root in .
-
Apply the chapter’s
gauss_eliminatehelper to the 4-by-4 matrix[[1, 2, 3, 4],[2, 3, 4, 5],[3, 4, 5, 6],[4, 5, 6, 7]]over . State the rank of the matrix and write one sentence explaining why the rank is what it is, in terms of a linear dependence among the rows of the original matrix. Hint: the rows form an arithmetic progression.
Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 2. A separate track, for rebuilding rather than reading: the package exercises/ch02-algebra has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch02 to grade your version against the suite that proves the reference one.
References
Section titled “References”Last updated: