1

not sure if this is the right place, but any help is greatly appreciated (noob here).

I am playing around with sagemath and I created the following problem ($A x = y$) in $GF(2^8)$ (which I hoped mirrored byte arithmetic which I see in C f.e). Specifically, let's say I would have given the matrix $A$ and $y$, which $x$ would solve the equation:

F.<a> = GF(2^8, modulus="minimal_weight")
A = Matrix(F, 3, 3)
x = [13, 83, 29]
y = [ 141, 40, 10 ]
m = [
    [23, 56, 2], 
    [92, 234, 182], 
    [12, 94, 132]
]

x = vector([ F.fetch_int(i) for i in x ]) y = vector([ F.fetch_int(i) for i in y ])

for i in range(len(m)): A.set_row(i, [ F.fetch_int(i) for i in m[i] ])

First of all, why is it in fact not mirroring the result I would expect from byte arithmetic (i. e. let's say I would implement the same thing in C using unsigned char and specifically ignore overflows):

#include <stdio.h>

int main() {

unsigned char A[3][3] = {
    { 23, 56, 2 },
    { 92, 234, 182 },
    { 12, 94, 132 }
};

unsigned char x[3] = { 13, 83, 29 };

for (int i = 0; i &lt; 3; ++i) {
    unsigned char sum = 0;
    for (int j = 0; j &lt; 3; ++j) {
        sum += x[j] * A[i][j];
    }
    printf(&quot;y[%d]: %d\n&quot;, i, (int)sum);
}

}

Which outputs [ 141, 40, 10 ]. But in sagemath the result is the following:

>>> A * x
[214, 224, 90]

Therefor obviously recovering the solution does not work either:

>>> A.inverse() * y
[194, 215, 241]

Well I was playing around, not understanding a lot, and tried the following:

F = Zmod(2^8)
A = Matrix(F, [
    [23, 56, 2], 
    [92, 234, 182], 
    [12, 94, 132]
])
x = vector(F, [13, 83, 29])
y = vector(F, [141, 40, 10 ])

With this one I at least get the expected result:

>>> A * x
[ 141, 40, 10 ]

.. however the matrix is not invertible anymore:

>>> A.is_invertible()
False

Obviously I have some kind of knowledge gap here and I am not sure where to go next. Thank you for your help.

milck
  • 167
  • 1
    Where do you want to go? What is the problem you are trying to solve? What is the context? Why are you using GF(256)? What do you mean by "byte arithmetic"? Help us help you. Please answer these questions in the body of your own question. – Somos Apr 03 '21 at 19:18
  • The error is in thinking that the arithmetic of $GF(2^8)$ is the same as that of $\Bbb{Z}_{2^8}$. In computer implementations of arithmetic of the fields $GF(2^n)$ we often (internally) represent the elements as chunks of $n$ bits, but that does not mean that their arithmetic would reflect that of integers in the range $[0..2^n-1]$ in any way whatsoever. For example, in this representation the additiion of two elements of $GF(2^n)$ is equal to bitwise XOR of those chunks of bits. – Jyrki Lahtonen Apr 04 '21 at 04:30
  • (cont'd) The multiplication is that of polynomials with coefficients in $GF(2)$ and degree $<n$ with overflows into degree $\ge n$ terms handled by reducing those polynomials modulo a defining polynomial of choice (= the minimal polynomial of the generating element $\alpha$, internally represented by the chunk of bits $00\ldots0010$ and thus occasionally also called $2$, because that chunk of bits also represents the integer $2$ in base two. – Jyrki Lahtonen Apr 04 '21 at 04:34
  • @ViktorVaughn People implementing the fields $GF(2^m)$ in programs often (internally to the program) represent the element $$z=b_0+b_1\alpha+b_2\alpha^2+\cdots+b_{m-1}\alpha^{m-1}\in GF(2^m),$$ $\alpha$ a root of a chosen irreducible polynomial of degree $m$ over $GF(2)$, as the chunk of $m$ bits $$z=b_{m-1}b_{m-2}\ldots b_2b_2b_0.\quad()$$ Those programmers who know what they are doing take the shortcut and also denote $z$ by the positive integer with base two representation $()$. So for example $\alpha^4+\alpha+1$ can be abbreviated to $19$ because $2^4+2+1=19$. – Jyrki Lahtonen Apr 04 '21 at 04:47
  • @JyrkiLahtonen I see. Thanks for the clarification! – Viktor Vaughn Apr 04 '21 at 04:50
  • (cont'd) See my comment above (or for example this).The trouble begins when those programmers who don't know this inside out do the same. And when such programmers meet people who are well versed in the algebra, but unfamiliar with this implementation trick, the resulting miscommunication is not fun to behold. If I had a dime for every attempt to clarify what's going to any party... I would not be rich, but I could afford one more bottle of fine single malt. – Jyrki Lahtonen Apr 04 '21 at 04:52
  • @milck You may benefit from studying this Q&A pair I once prepared with referrals in mind. It may not be exactly what you need to grasp to get a handle on the finite fields, but together with the other comments, it may do. – Jyrki Lahtonen Apr 04 '21 at 04:58
  • Thanks, I think I am starting to understand some of the problems I have. Essentially sage does not think of the numbers I give it the same way I understand them (i. e. sage treats them as coefficients to polynomials but I do not want that). So, is there any way of representing the desired problem in F.<a> = GF(2^8, modulus="minimal_weight")? Other than understanding that the representation is wrong, how do I (correctly) do what I intended to do? – milck Apr 04 '21 at 07:03

1 Answers1

1

Part of the answer is that the two questions in the question are in fact different questions.

The ring $\mathbb{Z}/2^8\mathbb{Z}$ of integers modulo $2^8$ is a very different ring from the field $F_{2^8}$ with $2^8$ elements. In particular it is not a field and not even an integral domain.

In Sage these two rings are Zmod(2^8) and GF(2^8).

Pursing that line of thought, given a linear system involving integers between 0 and 255, one can consider it as a system to be solved over $\mathbb{R}$, over $\mathbb{R}$, over $\mathbb{Z}/2^8\mathbb{Z}$, or over the finite field with $2^8$ elements, considering the integers as encodings of field elements.

In this answer we address solving modulo $2^8$ and solving in the finite field.

Solve a linear system modulo 256

Let's see how to define the system, find one solution, find all solutions.

The system

Define the ring $\mathbb{Z}/2^8\mathbb{Z}$, the matrix, the target vector:

sage: Z = Zmod(2^8)
sage: A = matrix(Z, 3, [[23, 56, 2],
....:                   [92, 234, 182],
....:                   [12, 94, 132]])
sage: y = vector(Z, 3, [141, 40, 10])

Check:

sage: A
[ 23  56   2]
[ 92 234 182]
[ 12  94 132]
sage: y
(141, 40, 10)

Find one solution

To find one solution to the system, i.e. a preimage of the target vector under the matrix, use the solve_right method:

sage: x = A.solve_right(y)
sage: x
(13, 83, 157)

Check:

sage: yy = A * x
sage: yy
(141, 40, 10)
sage: yy == y
True

Find all solutions

Finding all solutions is now equivalent to finding the right kernel of $A$ i.e. all vectors $x$ such that $A x$ is zero.

Sadly, finding that kernel is not implemented in Sage:

sage: A.right_kernel()
Traceback (most recent call last)
...
NotImplementedError: Echelon form not implemented
over 'Ring of integers modulo 256'.

The brute force solution is to try all vectors. There are $(2^8)^3$, i.e. $2^{24}$, i.e. 16,777,216 of them. Testing them all would take a while and waste resources.

Instead, we can observe that if $A x$ is zero in $\mathbb{Z}/2^8\mathbb{Z}$, then it is also zero modulo $\mathbb{Z}/2^k\mathbb{Z}$ for $k \in \{7, 6, 5, 4, 3, 2, 1, 0\}$.

For each of these $k$, call $A_k$ the matrix $A$ projected to $\mathbb{Z}/2^k\mathbb{Z}$.

The level $k = 0$ only has the zero matrix and the zero vector, so the kernel there is everything.

For the kernel of the projected matrix $A_1$, we can try all 8 vectors in $\mathbb{Z}/2^1\mathbb{Z}$, we find that 4 of them are part of the kernel.

For each $k$, each vector in the kernel of $A_{k-1}$ has $2^3$ lifts to $(\mathbb{Z}/2^k\mathbb{Z})^3$, which are the only candidates for forming the kernel of $A_k$.

For each starting vector in $\ker(A_1)$, we thus lift progressively, at each step there are eight vectors to try but only one of them ends up being in the kernel at the next level.

We end up computing 8 matrix times vector products in the first level (or computing a kernel basis by finding the row reduced echelon form over $\mathbb{Z}/2\mathbb{Z}$ which is a field, and then, for each of the 4 vectors of the level 1 kernel, 8 matrix times vector products in each of the next 7 levels.

In total, that means $8 + 4 \times 7 \times 8$, i.e. $225$ matrix times vector products. That's a lot more reasonable than the roughly 16.8 million matrix times vector products that brute force search would cost.

This function computes the kernel following that method:

def right_kernel_mod_p_to_the_k(A, p=None, k=None):
    r"""
    Return the kernel of ``A`` modulo ``p^k``
    """
    if p is None or k is None:
        Z = A.base_ring()
        f = Z.cardinality().factor()
        if len(f) == 1:
            p, k = f[0]
        else:
            raise ValueError("either specify p and k or use matrix with " 
                             "base ring some p^k with p prime and k > 0")
    n = A.nrows()
    W = (Zmod(p)^n).list()
    K = A.change_ring(Zmod(p)).right_kernel().list()
    for j in range(2, k + 1):
        R = Zmod(p^j)
        V = R^n
        B = A.change_ring(R)
        W = [p * V(v) for v in W]
        K = [V(x) for x in K]
        # candidates
        C = (u + v for u in K for v in W)
        K = [x for x in C if (B*x).is_zero()]
    return K

Compute the kernel with that function:

sage: K = right_kernel_mod_p_to_the_k(A)
sage: K
[(0, 0, 0), (0, 128, 0), (0, 0, 128), (0, 128, 128)]

Deduce the solutions to the original system:

sage: S = [x + u for u in K]
sage: S
[(13, 83, 157), (13, 211, 157), (13, 83, 29), (13, 211, 29)]

Check:

sage: [A*x for x in S]
[(141, 40, 10), (141, 40, 10), (141, 40, 10), (141, 40, 10)]

Solve a linear system over the finite field with 256 elements

This is a different problem. As given, it amounts to solving a linear system in $F_{2^8}$, with the extra steps of converting field elements from integer representation to polynomial representation and back.

A related question and answer are at

Given the severeal steps, we write a function:

def preimage(y, A):
    r"""
    Return a vector `x` over `F_{2^8}` such that `A x = y`.
Here `F_{2^8}` is the finite field with `2^8` elements
with minimal weight modulus.

INPUT:

- ``y`` -- a vector with entries in `F_{2^8}`
  where each entry is represented as an integer

- ``A`` -- a matrix with entries in `F_{2^8}`
  where each entry is represented as an integer

OUTPUT:

A vector ``x`` with entries in `F_{2^8}` such that `A x = y`.
Each entry is represented as an integer.
&quot;&quot;&quot;
F = GF(2^8, 'a', modulus='minimal_weight')
n = A.nrows()
AA = matrix(F, n, n, lambda i, j: F.fetch_int(A[i, j]))
yy = vector(F, n, [F.fetch_int(c) for c in y])
xx = AA.solve_right(yy)
return vector(ZZ, n, [c.integer_representation() for c in xx])

Example:

sage: A = matrix(ZZ, 3, [[23, 56, 2],
....:                    [92, 234, 182],
....:                    [12, 94, 132]])
sage: y = vector(ZZ, 3, [141, 40, 10])
sage: x = preimage(y, A)
sage: x
(194, 215, 241)

Here, finding the kernel would be easier, since the base ring of the relevant matrix is a field.

  • Thank you, but isn't that exactly what I did? Also you clearly are getting the wrong results too. From what I learned through the previous comments, GF(2^8) might not be what I actually want. The "source of truth" should always be the toy problem implemented in C – milck Apr 04 '21 at 10:29
  • Edited answer to address both versions of the question. – Samuel Lelièvre Apr 04 '21 at 15:40
  • Well that was easy, thank you. Do you, by any chance, know how one would get the other solutions? Through brute-force I found that there are four of them, though I suspect, that this quickly fails for larger systems. – milck Apr 04 '21 at 17:04
  • 1
    Edited again to compute kernel and find all solutions to the linear system without using brute force. – Samuel Lelièvre Apr 05 '21 at 00:37