DEV Community

Cover image for Building Post-Quantum Cryptography from Scratch: CRYSTALS-Kyber in Python (Part 1/n)
Jani
Jani

Posted on

Building Post-Quantum Cryptography from Scratch: CRYSTALS-Kyber in Python (Part 1/n)

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 aa and bb are congruent modulo q,q, we mean bab-a is a integer multiple of q.q. In this case we write

ab  (mod  q).a\equiv b\;(\bmod\; q).

With r=a  (mod  q)r = a\;(\bmod\; q) we mean rr is the remainder of integer aa divided q.q. This implies 0r<q.0\leq r \lt q.

Zq=0,1,2,...,q1\mathbb{Z}_q = {0,1,2,...,q-1} is the ring of integers modulo q.q. In this ring addition and multiplication are performed modulo q.q.

We implement integers in Zq\mathbb{Z}_q 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
Enter fullscreen mode Exit fullscreen mode

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 Z4=0,1,2,3.\mathbb{Z}_4={0,1,2,3}. For example we have

29=7=1  (mod  4)2-9=-7=1\;(\bmod\; 4)
3+2=5=1  (mod  4)3+2=5=1\;(\bmod\; 4)

and for multiplication

34=12=0  (mod  4)3*4=12=0\;(\bmod\; 4)
35=15=3  (mod  4).3*5=15=3\;(\bmod\; 4).
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
Enter fullscreen mode Exit fullscreen mode

Polynomial Rings

Let qq be a prime. We define Zq[x]\mathbb{Z}_q[x] to be the set of polynomials of xx with all coefficients in the ring Zq.\mathbb{Z}_q. This means all coefficient arithmetic is performed in the ring Zq.\mathbb{Z}_q.

We implement polynomials in the ring with a class ZqPolynomial. Here coefficients is a list of integers, and the length of the list defines n.n.

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)
Enter fullscreen mode Exit fullscreen mode

For example, let q=5,q=5,

f(x)=3+2x+4x2+3x3f(x)=3+2x+4x^2+3x^3

and

h(x)=2+x+4x2+4x3+x4.h(x)=2+x+4x^2+4x^3+x^4.

We get

f(x)+h(x)=3x+3x2+2x3+x4,f(x)+h(x)=3x+3x^2+2x^3+x^4,
f(x)h(x)=1+x+4x3+4x4f(x)-h(x)=1+x+4x^3+4x^4
f(x)h(x)=1+2x+2x2+x6+3x7.f(x)h(x)=1+2x+2x^2+x^6+3x^7.

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
Enter fullscreen mode Exit fullscreen mode

Let now qq be a prime and nn a positive integer. The quotient ring (often called just "polynomial ring") Rq=Zq[x]/(xn+1)R_q = \mathbb{Z}_q[x]/(x^n+1) consists of polynomials in Zq\mathbb{Z}_q of degree less than n.n. In ring RqR_q the multiplication of polynomials is performed modulo the polynomial xn+1x^n+1 called the reduction polynomial. This means that the product of polynomials f(x),g(x)Zq[x]f(x),g(x)\in \mathbb{Z}_q[x] is defined as the remainder r(x)r(x) of their product when divided by xn+1x^n+1 in the polynomial ring. Notice that by definition now degree of r(z)r(z) is at most n1n-1 and

f(x)g(x)=r(x)Zq[x].f(x)g(x)= r(x)\in\mathbb{Z}_q[x].

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 n\geq n you can apply the rules

xn1x^n\equiv -1
x(n+1)xx^{(n+1)}\equiv -x
and in general for kn,k\geq n, x(n+k)xk,x^{(n+k)}\equiv -x^k, and then simplify the resulting polynomial normally. To understand why, please visit ring theory and ideals.

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)
Enter fullscreen mode Exit fullscreen mode

For example, consider the ring

Z5[x]/(x4+1)\mathbb{Z}_5[x]/(x^4+1)

and polynomials

f(x)=1+3x+4x2+x3f(x)=1+3x+4x^2+x^3

and

g(x)=x+3x2+3x3.g(x)=x+3x^2+3x^3.

To get the reminder of f(x)g(x)f(x)g(x) when divided x4+1x^4+1 we first calculate the product

f(x)g(x)=x+6x2+16x3+22x4+15x5+3x6f(x)g(x)=x+6x^2+16x^3+22x^4+15x^5+3x^6
and use the substitution rule to get
f(x)g(x)=2214x+3x2+16x3f(x)g(x)=-22-14x+3x^2+16x^3
and with the modulo operations we arrive at
f(x)g(x)=3+x+3x2+x3Z5[x]/(x4+1).f(x)g(x)=3+x+3x^2+x^3\in\mathbb{Z}_5[x]/(x^4+1).

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
Enter fullscreen mode Exit fullscreen mode

Polynomials as Vectors

For a programmer it is rather straightforward to see that the polynomial can be represented as vectors. Consider the polynomial

f(x)=a0+a1x+a2x2++an1xn1Zq[x]/(xn+1).f(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}\in\mathbb{Z}_q[x]/(x^n+1).

The obvious way to write that as a vector is f=[a0,a1,...,an1]Tf=[a_0,a_1,...,a_{n-1}]^T (convention is to use column vectors). The polynomial addition is now component-wise addition modulo q,q, and subtraction is component-wise subtraction modulo q.q. 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 k>0k>0 be a positive integer. Module RqkR^k_q consists of length kk vectors of polynomials of RqR_q . Addition and subtraction is again component-wise. It follows that the resulting vectors in both cases is in Rqk.R_q^k.

We will later use Rqk×lR_q^{k\times l} to represent k×lk\times l -matrices with polynomial entries. This extension is similar to that from vectors to matrices in linear algebra.

The module RqkR_q^k includes k×1k\times 1 -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 RqkR_q^k is defined as inner product of two vectors in Rqk.R_q^k. This means that the polynomials, that are elements of the vector in Rqk,R_q^k, are multiplied in RqR_q and added together. The result is in Rq.R_q.

For example, let

A=(4+x+x2x+3x2)R52A=\begin{pmatrix}4+x+x^2 \newline x+3x^2\end{pmatrix}\in R_5^2
and
B=(1+2x+3x24x)R52.B=\begin{pmatrix}1+2x+3x^2 \newline 4x\end{pmatrix}\in R_5^2.
We get directly
A+B=(3x3x2)R52A+B=\begin{pmatrix} 3x \newline 3x^2\end{pmatrix}\in R_5^2
AB=(3+4x+4x22x+3x2)R52A-B=\begin{pmatrix} 3+4x+4x^2 \newline 2x+3x^2\end{pmatrix}\in R_5^2
and in Python as follows.
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]
Enter fullscreen mode Exit fullscreen mode

Using the AA and BB defined earlier, we get

ATB=(4+x+x2,x+3x2)[1+2x+3x24x]A^TB=(4+x+x^2,x+3x^2)\begin{bmatrix}1+2x+3x^2\newline 4x\end{bmatrix}
=(4+x+x2)(1+2x+3x2)+(x+3x2)(1+2x+3x2)=(4+x+x^2)(1+2x+3x^2)+(x+3x^2)(1+2x+3x^2)
=3x.=3x.



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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 qZq\in\mathbb{Z} be odd and rZqr\in\mathbb{Z}_q . We define

rmodsq={rif r(q1)/2rqifr>(q1)/2.r\bmod_s q = \begin{cases} r & \mathrm{if\ } r\leq (q-1)/2 \newline r-q & \mathrm{if } r > (q-1)/2. \end{cases}

This immediately gives (q1)/2rmodsq(q1)/2.-(q-1)/2\leq r\bmod_s q\leq (q-1)/2. Here mods\bmod_s is symmetric modulo operation.

Let q=11.q=11. We now have by definition 5rmods115.-5\leq r\bmod_s 11\leq 5.

For example

4mods11=48mods11=3.\begin{align*}4\bmod_s 11 &=4 \newline 8\bmod_s 11 &= -3.\end{align*}

The definition of symmetric modulo is slightly different for even q.q. Let qZq\in\mathbb{Z} be even and rZq.r\in\mathbb{Z}_q.

Then

rmodsq={rif rq/2rqif r>q/2.r\bmod_s q = \begin{cases} r & \mathrm{if\ } r\leq q/2 \newline r-q & \mathrm{if\ } r > q/2.\end{cases}
and we get q/2<rmodsqq/2-q/2< r\bmod_s q\leq q/2 .

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
Enter fullscreen mode Exit fullscreen mode

Let qZ.q\in\mathbb{Z}. We define

r=rmodsq.|r|{\infty} = |r\bmod_s q|.



For example, let

q=11.q=11.

Then

5=58=3=310=1=1.\begin{align*} |5|{\infty} &= 5 \newline |8|{\infty} &=|-3|= 3\newline |10|{\infty} &= |-1| = 1.\end{align*}



You can think this as "clock-algebra", the further the integer is from noon, the bigger its norm.

We have the following immediate corollaries

0r(q1)/20\leq |r|{\infty}\leq (q-1)/2
if q is odd and
0rq/20\leq |r|{\infty}\leq q/2
if q is even.

For polynomial ring elements the size of a polynomial is defined with the maximum operation. Let

f(x)=a0+a1x+a2x2+a3x3++an1xn1.f(x)=a_0+a_1x+a_2x^2+a_3x^3+\cdots +a_{n-1}x^{n-1}.

We define
f(x)=maxiai.|f(x)|{\infty} = \max_i |a_i|{\infty}.

For example, let q=11,q=11, n=4n=4 and
f(x)=8+x+5x2+3x3Zq/(xn+1).f(x)=8+x+5x^2+3x^3\in\mathbb{Z}q/(x^n+1).
Then f(x)=5|f(x)|{\infty}=5 because
a0=8mods11=3=3a1=1mods11=1=1a2=5mods11=5=5a3=3mods11=3=3\begin{align*} |a_0|{\infty} &= |8\bmod_s 11| &=|-3| &= 3\newline |a_1|{\infty} &= |1\bmod_s 11| &=|1| &= 1\newline |a_2|{\infty} &= |5\bmod_s 11| &=|5| &= 5\newline |a_3|{\infty} &= |3\bmod_s 11| &=|3| &= 3\end{align*}
and clearly maxiai=5.\max_i |a_i|_{\infty} =5.

This definition can be generalized to elements of module Rqk.R_q^k. Let

a=[a1,a2,...,ak]TRqk.a=[a_1,a_2,...,a_k]^T\in R_q^k.
We define
a=maxiai.|a|{\infty}=\max_i|a_i|{\infty}.

Small Polynomials

We say a polynomial ff is small if f|f|_{\infty} is small. Notice that this means that all coefficients of ff need to be small due the way the norm is defined. What "small" means is defined per context.

Let η\eta be a positive integer less than q/2.q/2. We define

Sη={fRqfη}S_\eta = \lbrace f\in R_q\,|\,|f|{\infty}\leq \eta\rbrace
to be the set of polynomials in RqR_q where each polynomials each coefficient is of size at most η.\eta. We use SηS\eta to define a set of "small" polynomials.

For example consider polynomial

f(x)=10+9x+9x2+2x3R11.f(x)=10+9x+9x^2+2x^3\in R_{11}.
Now
f(x)=2|f(x)|_{\infty}=2
and hence f(x)S2.f(x)\in S_2.

Observe that S1S_1 is the set of polynomials in RqR_q with all coefficients in the set {1,0,1}\lbrace -1,0,1\rbrace (when reduced modsq\bmod_s q ).

Let f(x)Sη1f(x)\in S_{\eta_1} and g(x)Sη2.g(x)\in S_{\eta_2}. Without proof we state that f(x)g(x)Snη1η2.f(x)g(x)\in S_{n\eta_{1}\eta_{2}}. This generalizes to vectors or polynomials. Let aSkη1a\in S^{k}{\eta{1}} and bSkη2.b\in S^{k}{\eta_2}. Then we have abTSknη1η2.ab^T\in S{kn\eta_{1}\eta_{2}}.

Next steps

In the next article we utilize the presented machinery to implement basic CRYSTALS-Kyber public key encryption. Stay tuned.

Top comments (0)