We will explore how to implement a post-quantum cryptography algorithm(s) CRYSTALS. We start the journey by introducing some needed tools and in the following posts implement the actual algorithm. If you are interested in the dirty details how to turn math to code, this is for you.
[This article was written in another platform first and I had some painful time to get it rendering even remotely correct here. I hope it is readable enough.]
Introduction
CRYSTALS ("Cryptographic Suite for Algebraic Lattices") is post-quantum public key encryption scheme, meaning it is expected to be secure even at the era of quantum computing where many current PKE-variants fail.
CRYSTALS consists of two cryptographic primitives: Kyber and Dilithium. Kyber is key exchange method, i.e. asymmetric encryption providing secure channel to change secrets, and Dilithium is a cryptographic signing method. We will explore the mathematics behind these algorithms by coding them in Python as we go. You can find the code from my GitHub repo.
The reader is assumed certain maturity in mathematics and basic understanding of Python. We don't prove anything, instead the focus is to introduce and build needed machinery that we will use later.
The mathematical part of this presentation follows closely the way Alfred Menezes presents it in his excellent video series on the topic.
Modulo Algebra
When we say two integers
and
are congruent modulo
we mean
is a integer multiple of
In this case we write
With we mean is the remainder of integer divided This implies
is the ring of integers modulo In this ring addition and multiplication are performed modulo
We implement integers in
in class Zq. Notice the Python modulo-operation % is implemented in a way that is fully compatible with our needs because it can handle negative values correctly. The instantiation can be done with integer value or with an instance of Zq.
class Zq:
def __init__(self, q, value):
# Sanity checks cleaned
if isinstance(value,int):
# Python modulo operation is one of the few correctly implemented
self.value = value % q
else:
self.value = value.get_value() % q
self.q = q
The class Zq has addition, subtraction, multiplication, str and repr operations implemented. This makes our life a lot easier because we can make arithmetics directly and debug when needed.
To get a feeling how things work, consider the ring For example we have
and for multiplication
if __name__ == "__main__":
two = Zq(4,2)
three = Zq(4,3)
four = Zq(4,4)
five = Zq(4,5)
nine = Zq(4,9)
print(f"2-9={two-nine}")
print(f"3+2={three+two}")
print(f"3*4={three*four}")
print(f"3*5={three*five}")
(.venv) ..> python Zq.py
2-9=1
3+2=1
3*4=0
3*5=3
Polynomial Rings
Let be a prime. We define to be the set of polynomials of with all coefficients in the ring This means all coefficient arithmetic is performed in the ring
We implement polynomials in the ring with a class ZqPolynomial. Here coefficients is a list of integers, and the length of the list defines
class ZqPolynomial:
def __init__(self, q, coefficients):
# Sanity checks cleaned
self.q = q
self.coefficients = coefficients
for i in range(len(coefficients)):
self.coefficients[i] = Zq(self.q, self.coefficients[i])
# n from (x^n+1)
self.n = len(coefficients)
For example, let
and
We get
We can do this with our code as follows (we use extra zeroes in coefficients to prevent the polynomial modulo operation).
if __name__ == "__main__":
f = ZqPolynomial(5, [3, 2, 4, 3, 0,0,0,0])
h = ZqPolynomial(5, [2, 1, 4, 4, 1,0,0,0])
print(f"f(x)+h(x)={f+h}")
print(f"f(x)-h(x)={f-h}")
print(f"f(x)h(x)={f*h}")
> python zq_polynomial.py
f(x)+h(x)=3x+3x^2+2x^3+x^4
f(x)-h(x)=1+x+4x^3+4x^4
f(x)h(x)=1+2x+2x^2+x^6+3x^7
Let now be a prime and a positive integer. The quotient ring (often called just "polynomial ring") consists of polynomials in of degree less than In ring the multiplication of polynomials is performed modulo the polynomial called the reduction polynomial. This means that the product of polynomials is defined as the remainder of their product when divided by in the polynomial ring. Notice that by definition now degree of is at most and
One should notice here that remainder is not calculated by the traditional polynomial division algorithm, but with division rules that apply in the polynomial ring. For our purposes it suffices to acknowledge that if the polynomial has degrees you can apply the rules
Overloading addition and subtraction is straightforward, but multiplication needs special treatment. Here we utilize the fact that Zq has multiplication operation overloaded. In real-life implementations this naive implementation is too slow and NTT-algorithm is used instead. We will return to this later.
def __mul__(self, other): # Again, sanity checks removed
n = len(self.coefficients)
# Initialize result coefficients with zeros
result = [Zq(self.q, 0) for _ in range(n)]
# Perform polynomial multiplication
for i in range(n):
for j in range(n):
# Position in the product
pos = i + j
# If position exceeds n-1, we need to reduce modulo x^n + 1
if pos >= n:
# When reducing mod x^n + 1, x^n = -1
# So x^(n+k) = -x^k
pos = pos - n
result[pos] -= self.coefficients[i] * other.coefficients[j]
else:
result[pos] += self.coefficients[i] * other.coefficients[j]
return ZqPolynomial(self.q, result)
For example, consider the ring
and polynomials
and
To get the reminder of when divided we first calculate the product
With our code we get directly as follows.
if __name__ == "__main__":
f = ZqPolynomial(5,[1,3,4,1])
g = ZqPolynomial(5,[0,1,3,3])
print(f"f*g={f*g}")
>python zq_polynomial.py
f*g=3+x+3x^2+x^3
Polynomials as Vectors
For a programmer it is rather straightforward to see that the polynomial can be represented as vectors. Consider the polynomial
The obvious way to write that as a vector is (convention is to use column vectors). The polynomial addition is now component-wise addition modulo and subtraction is component-wise subtraction modulo Multiplication is polynomial multiplication as shown earlier and the resulting polynomial is stored in a vector.
We used this implicitly earlier when defining ZqPolynomial.
We extend this notation. Let be a positive integer. Module consists of length vectors of polynomials of . Addition and subtraction is again component-wise. It follows that the resulting vectors in both cases is in
We will later use to represent -matrices with polynomial entries. This extension is similar to that from vectors to matrices in linear algebra.
The module includes -matrices with elements from the polynomial ring. The easiest way to handle these is to define class PolyMatrix that holds ZqPolynomials in a list of lists.
The multiplication in is defined as inner product of two vectors in This means that the polynomials, that are elements of the vector in are multiplied in and added together. The result is in
For example, let
if __name__ == "__main__":
A = PolyMatrix(2,1,5,3)
A[0,0] = ZqPolynomial(5,[4,1,2])
A[1,0] = ZqPolynomial(5,[0,1,3])
B = PolyMatrix(2,1,5,3)
B[0,0] = ZqPolynomial(5,[1,2,3])
B[1,0] = ZqPolynomial(5,[0,4,0])
print(f"A+B={A+B}")
print(f"A-B={A-B}")
(.venv)...> python poly_matrix.py
A+B=[3x]
[3x^2]
A-B=[3+4x+4x^2]
[2x+3x^2]
Using the
and
defined earlier, we get
And in Python.
if __name__ == "__main__":
A = PolyMatrix(2,1,5,3)
A[0,0] = ZqPolynomial(5,[4,1,2])
A[1,0] = ZqPolynomial(5,[0,1,3])
B = PolyMatrix(2,1,5,3)
B[0,0] = ZqPolynomial(5,[1,2,3])
B[1,0] = ZqPolynomial(5,[0,4,0])
print(f"A.T@B={A.T@B}")
(.venv)..>python poly_matrix.py
A.T@B=[3x]
To enable matrix multiplication, we implemented the matmul operator.
def __matmul__(self, other): # enables @ operator
result = PolyMatrix(self.rows, other.cols, self.q, self.n)
for i in range(self.rows):
for j in range(other.cols):
# Sum of products for this position
for k in range(self.cols):
result[i, j] += self[i, k] * other[k, j]
return result
We can use the bracket-notation with PolyMatrix because we defined getitem and setitem methods.
Size of Polynomials
Next we need notion of size that will become useful later. First let us define symmetric mod.
Let be odd and . We define
This immediately gives Here is symmetric modulo operation.
Let We now have by definition
For example
The definition of symmetric modulo is slightly different for even Let be even and
Then
In the code we implement the symmetric modulo in Zq and use that from ZqPolynomial and PolyMatrix.
def to_symmetric(self):
"""Convert value to symmetric representation in [-(q-1)/2, q/2]"""
if self.q % 2 == 0: # even q
if self.value >= self.q // 2:
self.value = self.value - self.q
else: # odd q
if self.value > self.q // 2:
self.value = self.value - self.q
return self
Let We define
For example, let
Then
You can think this as "clock-algebra", the further the integer is from noon, the bigger its norm.
We have the following immediate corollaries
For polynomial ring elements the size of a polynomial is defined with the maximum operation. Let
We define
For example, let and
This definition can be generalized to elements of module Let
Small Polynomials
We say a polynomial is small if is small. Notice that this means that all coefficients of need to be small due the way the norm is defined. What "small" means is defined per context.
Let be a positive integer less than We define
For example consider polynomial
Observe that is the set of polynomials in with all coefficients in the set (when reduced ).
Let and Without proof we state that This generalizes to vectors or polynomials. Let and Then we have
Next steps
In the next article we utilize the presented machinery to implement basic CRYSTALS-Kyber public key encryption. Stay tuned.
Top comments (0)