LoginSignup
1
0

More than 1 year has passed since last update.

GCMにおけるガロア体の計算方法

Posted at

GCMの認証コードの計算において、ガロア体 $GF(2^{128})$ におけるハッシュ鍵 $L$ とのかけ算が出てきます。

過去記事の補足で、一般的なかけ算のアルゴリズムを書きましたが、固定値と任意の値とのかけ算と考えて高速化することができます。ただし、大きなサイズのテーブルを作成するため、メモリ使用量とのトレードオフになります。

参考文書

[1] The Galois/Counter Mode of Operation (GCM)
https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
4 Implementation
4.1 Software

[2]【過去記事】認証暗号化モードGCMの理論的なところの解説
https://qiita.com/shirokumaneko/items/5185373a9f0aebb16251
補足
ガロア体について

一番速い方法 (Simple table)

その代わり、メモリ使用量も最大です。

\begin{align}
a &= \sum_{i=0}^{127} a_i x^i \\
&= \sum_{i=0}^{15} \left(\sum_{j=0}^7 a_{ij}x^j \right) x^{8i} \\
&= \sum_{i=0}^{15} a^{(i)}x^{8i}
\end{align}

のようにおくと、

\begin{align}
L \cdot a = \sum_{i=0}^{15} (Lx^{8i})a^{(i)}
\end{align}

$a^{(i)} \in GF(2^8)$ であり、ビット列で考えると8ビット、0~255の値を取ります。
各 $i$ について、$a^{(i)} = 00000000 \cdots 11111111$ のすべての場合の $(Lx^{8i})a^{(i)}$ の値をあらかじめ計算しておきます。

M_i[n_{a^{(i)}}] = (Lx^{8i})a^{(i)}

ここで、$n_{a^{(i)}}$ は $a^{(i)}$ のビット列を8ビットの値とみなしたものです。
つまり、

\begin{align}
M_0[0] &= (Lx^0) \cdot 0 = 0 \\
M_0[1] &= (Lx^0) \cdot x^7 \\
M_0[2] &= (Lx^0) \cdot x^6 \\
&\ \ \ \ \ \vdots \\
M_{15}[255] &= (Lx^{120})(x^7 + \cdots + x + 1)
\end{align}

というような感じです。
こうすると、

L \cdot a = \sum_{i=0}^{15} M_i[n_{a^{(i)}}]

となります。
なお、GCMではビット順をリトルエンディアンとするため、1 = 0x01 = $x^7$, 128 = 0x80 = $x^0$ になります。

class GF2_128
{
public:
    GF2_128(void);
    GF2_128 multiply_L(void);
    unsigned char coef[16];
};

GF2_128 M[16][256];
GF2_128 GF2_128::multiply_L(void)
{
    GF2_128 a;
    for (int i = 0; i < 16; i++)
    {
        a += M[i][this->coef[i]];
    }
    return a;
}

かけ算がないので、一見して速そうなのがわかると思います。
$i = 0,\cdots,15$ のそれぞれに256個の $GF(2^{128})$ なので、16×256×16 = 65536 = 64KBのテーブルになります。

なお、

\begin{align}
L \cdot a = \sum_{i=0}^{31} (Lx^{4i})a^{(i)}\ (a^{(i)} = \sum_{i=0}^3 a_{ij}x^j)
\end{align}

のように分割すると、$a^{(i)}$ は4ビット、0~15の値を取るので、32×16×16 = 8192 = 8KBになります。

そこそこ速い方法 (Shoup's table)

その分、メモリ使用量は小さいです。具体的には $M_0$ だけを使うので4KBです。

\begin{align}
L \cdot a &= L \cdot \sum_{i=0}^{15} a^{(i)} x^{8i} \\
&= \sum_{i=0}^{15} (La^{(i)}) \cdot x^{8i} \\
&= (((La^{(15)} \cdot x^8 + La^{(14)}) \cdot x^8 + La^{(13)}) \cdot x^8 + \cdots\ ) \cdot x^8 + La^{(0)} \\
\end{align}

$La^{(i)}$ はテーブル $M_0$ の値なので、

L \cdot a = (((M_0[n_{a^{(15)}}] \cdot x^8 + M_0[n_{a^{(14)}}]) \cdot x^8 + M_0[n_{a^{(13)}}]) \cdot x^8 + \cdots\ ) \cdot x^8 + M_0[n_{a^{(0)}}]
GF2_128 GF2_128::multiply_L(void)
{
    GF2_128 a = GF2_128(M[0][this->coef[15]]);
    for (int i = 15; i >= 1; i--)
    {
        a = a.multiply_x8();
        a += M[0][this->coef[i-1]];
    }
    return a;
}

この方法は、メモリ使用量が少ない代わりに $x^8$ とのかけ算が必要になります。

テスト

過去記事で使用したハッシュ鍵を使い、GHASHの計算結果を確認します。

unsigned char L[16] =
{
    0xB8, 0x3B, 0x53, 0x37, 0x08, 0xBF, 0x53, 0x5D, 0x0A, 0xA6, 0xE5, 0x29, 0x80, 0xD5, 0x3B, 0x78, 
};
void GCM::GHASH(unsigned char *C, GF2_128& S, int blockNum)
{
    S = GF2_128();                     // S[0] = 0^{128}
    for (int i = 0; i < blockNum; i++)
    {
        S += GF2_128(&C[16*i]);        // S[i-1] + C[i]
        S = S.multiply_L_simple();     // (S[i-1] + C[i])・L
        // S = S.multiply_L_shoup();
    }
}
TEST(GCM_AES128, Example2)
{
    GCM gcm(K, IV);
    memcpy(C, M, sizeof(M));
    gcm.authenticated_encrypt(M, C, T, sizeof(M));
    EXPECT_EQ(memcmp(S.coef, S_exp, 16), 0);
}

実行結果
simple,shoupとも

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from GCM_AES128
[ RUN ] GCM_AES128.Example2
[ OK ] GCM_AES128.Example2 (0 ms)
[----------] 1 test from GCM_AES128 (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (4 ms total)
[ PASSED ] 1 test.

実行時間

Raspberry Pi 4で

g++ -O2

テーブル authenticated_encrypt create
Simple (64KB) 37.8 usec 282.0 usec
Shoup (4KB) 41.8 usec 35.5 usec
なし 114.0 usec
int main(void)
{
    GCM gcm(K, IV);
    memcpy(C, M, sizeof(M));

    struct timespec ts_start, ts_end;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_start);
    gcm.authenticated_encrypt(M, C, T, sizeof(M));
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_end);

    printf("%d usec\n", (ts_end.tv_nsec - ts_start.tv_nsec) / 1000);
}

補足

テーブルの作成方法

$M_i$ の定義は

M_i[n_{a^{(i)}}] = (Lx^{8i})a^{(i)}

ですが、

\begin{align}
M_0[128] = L &\cdot x^{0} = L \\
M_0[64] = L &\cdot x\ \ = M_0[128] \cdot x \\
&\vdots \\
M_0[1] = L &\cdot x^7 = M_0[2] \cdot x \\
\end{align}

として $2^n$ 系列を先に求めてから

\begin{align}
&M_0[3] = L \cdot (x^{127} + x^{126}) = M_0[1] + M_0[2]
\end{align}

のようにして、すべての値を計算することができます。

M[0][128] = L;

for (int j = 64; j > 0; j /= 2)
{
    M[0][j] = M[0][2*j].multiply_x(); // M_0[64], ..., M_0[1] = L・x^7
}

for (int j = 2; j <= 128; j *= 2)
{
    for (int k = 1; k < j; k++)
    {
        M[0][j+k] = M[0][j] + M[0][k];
    }
}

$x$ とのかけ算multiply_xは、過去記事で上げたGF2_128_multiply_xと同じです。

GF2_128 GF2_128::multiply_x(void)
{
    GF2_128 a;
    int i;
    unsigned char a_127 = (this->coef[15] & 0x01);

    // a >> 1
    for (i = 15; i > 0; i--)
    {
        a.coef[i] = (this->coef[i] >> 1) | (this->coef[i - 1] << 7);
    }
    this->coef[0] = this->coef[0] >> 1;

    if (a_127 == 1)
    {
        a.coef[0] ^= 0xe1; // 1 + x + x^2 + x^7
    }

    return a;
}

$M_1, \cdots, M_{15}$ は、出発点が $M_1[128] = L \cdot x^8, \cdots, M_{15}[128] = L \cdot x^{120}$ となる以外は、同じ手順で計算できます。

for (int i = 0; i < 16; i++)
{
    M[i][128] = L;
    for (int j = 0; j < i; j++)
    {
        M[i][128] = M[i][128].multiply_x8(); // M_i[128] = L・x^{8i}
    }
}

$x^8$ とのかけ算は、$x$ とのかけ算を8回繰り返してもいいですが、これも高速化できます。

\begin{align}
a \cdot x^8 &= \sum_{i=0}^{15} a^{(i)} x^{8(i+1)} \\
&= \sum_{i=1}^{15} a^{(i-1)} x^{8i} + a^{(15)} x^{128} \\
&= \sum_{i=1}^{15} a^{(i-1)} x^{8i} + a^{(15)}(x^7 + x^2 + x + 1)
\end{align}

$x^7 + x^2 + x + 1$ とのかけ算をあらかじめ計算しておき、

R[n_{a^{(15)}}] = a^{(15)}(x^7 + x^2 + x + 1)

とすれば、

GF2_128 GF2_128::multiply_x8(void)
{
    GF2_128 a;
    for (int i = 15; i >= 1; i--)
    {
        a.coef[i] = this->coef[i-1]; // a^{(i-1)} x^{8i}
    }
    a += R[this->coef[15]];          // a^{(15)}(x^7 + x^2 + x + 1)
    return a;
}

ここで、$a^{(15)} = \sum_{j=0}^7 a_j^{(15)} x^j$ なので、$a^{(15)}(x^7 + x^2 + x + 1)$ は128次以上の項を生じません。
つまり、単なるビットシフトになります。

R[n_{a^{(15)}}] = (a^{(15)} >> 7) \oplus (a^{(15)} >> 2) \oplus (a^{(15)} >> 1) \oplus a^{(15)}

最終的には、以下のようになります。

GF2_128 M[16][256];
GF2_128 R[256];

void table_create(GF2_128& L)
{
    table_createR();
    table_createM(L);
}

void table_createR(void)
{
    for (int n = 0; n < 256; n++)
    {
        R[n].coef[0] = (n >> 7) ^ (n >> 2) ^ (n >> 1) ^ n;
        R[n].coef[1] = ((n & 0x7f) << 1) ^ ((n & 0x03) << 6) ^ ((n & 0x01) << 7);
    }
}

void table_createM(GF2_128& L)
{
    for (int i = 0; i < 16; i++)
    {
        M[i][128] = L;
        for (int j = 0; j < i; j++)
        {
            M[i][128] = M[i][128].multiply_x8(); // M_i[128] = L・x^{8i}
        }
    }

    for (int i = 0; i < 16; i++)
    {
        for (int j = 64; j > 0; j /= 2)
        {
            M[i][j] = M[i][2*j].multiply_x();
        }
        for (int j = 2; j <= 128; j *= 2)
        {
            for (int k = 1; k < j; k++)
            {
                M[i][j+k] = M[i][j] + M[i][k];
            }
        }
    }
}

$x^8$ の計算に必要なので、$R$ は $M_i$ よりも先に計算する必要があります。

なお、AESの秘密鍵 $K$ を交換すると $L = E_K(0)$ も変わるので、テーブルをつくり直す必要があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0