💬

C言語でDH鍵交換とBaby-step Giant-step アルゴリズムの実装

に公開

はじめに

この記事では C 言語を使用して Diffie-Hellman 鍵共有と Baby-step Giant-step アルゴリズムを実装します。Diffie-Hellman 鍵共有は、2 人の当事者が安全に共通の秘密鍵を生成するためのプロトコルです。離散対数問題は、公開鍵暗号の基礎となる数学的問題です。

この記事は、以下を参考にしています。

https://zenn.dev/herumi/articles/sd202203-ecc-1

https://tjkendev.github.io/procon-library/python/math/baby-step-giant-step.html

数学的準備

DH 鍵共有のための数学的な準備としていくつか準備が必要です。

離散対数問題

離散対数問題は、与えられた整数 g, p, A に対して、

g^a \equiv A \mod p

を満たす a を求める問題です。この問題は計算量的に困難であり、特に大きな素数pに対しては効率的なアルゴリズムが知られていません。

指数関数

離散対数を解くためには、指数関数を効率的に計算する必要があります。

g^a \mod p

この計算は繰り返し二乗法を使用して効率的に行うことができ、計算量は O(\log a) です。

C 言語による繰り返し二乗法の実装例
// Result = (base^exp) % mod
int mod_pow(int base, int exp, int mod) {
    unsigned long long result = 1;
    unsigned long long b = base % mod;

    while (exp) {
        if (exp & 1) {
            result = (result * b) % mod;
        }
        exp >>= 1;
        b = (b * b) % mod;
    }
    return (int)result;
}

整数平方根

後に述べる Baby-step Giant-step アルゴリズムで使用するため、整数の平方根を計算するための関数を実装します。

C 言語による整数平方根の実装例
int isqrt(int n) {
    if (n < 0) return -1; // Invalid input
    if (n == 0 || n == 1) return n;

    unsigned long long x = n;
    unsigned long long y = (x + 1) / 2;

    while (y < x) {
        x = y;
        y = (x + n / x) / 2;
    }

    return (int)x;
}

DH 鍵共有

数学的な準備が整ったところで、DH 鍵共有の手順と C 言語での実装例を示します。

DH 鍵共有の手順

Diffie-Hellman 鍵共有は、2 人が安全に共通の秘密鍵を生成するためのプロトコルです。まず、DH 鍵共有の手順を説明します。

  1. Alice と Bob は、大きな素数 p と生成元 g を共有します。
  2. Alice と Bob はそれぞれ秘密鍵 ab を選びます。
  3. Alice は公開鍵 A = g^a \mod p を計算し、Bob に送信します。
  4. Bob は公開鍵 B = g^b \mod p を計算し、Alice に送信します。
  5. Alice は受け取った Bob の公開鍵 B を使って共通鍵 K = B^a \mod p を計算します。
  6. Bob は受け取った Alice の公開鍵 A を使って共通鍵 K = A^b \mod p を計算します。
  7. これにより、Alice と Bob は同じ共通鍵 K を持つことになります。

この共有プロセスでは、離散対数問題の難しさに依存しており、第三者が gp を知っていても、AB から秘密鍵 ab や共通鍵 K を計算することは困難です。なぜなら、AB から秘密鍵を求めることは離散対数問題を解くことに等しいからです。

手計算による確認

上記の計算を手計算にて確認します。

まず、p = 1009, g = 11 とします。Alice の秘密鍵 a = 123 とし、Bob の秘密鍵 b = 456 とします。次に、公開鍵を計算します。

\begin{aligned} A & \equiv g^a & \mod p \\ & \equiv 11^{123} & \mod 1009 \\ & \equiv 510 & \mod 1009 \end{aligned}
\begin{aligned} B & \equiv g^b & \mod p \\ & \equiv 11^{456} & \mod 1009 \\ & \equiv 185 & \mod 1009 \end{aligned}

Alice は Bob にAを送り、Bob は Alice にBを送ります。最後にそれぞれ共通鍵を計算します。

\begin{aligned} K & \equiv B^a & \mod p \\ & \equiv 185^{123} & \mod 1009 \\ & \equiv 74 & \mod 1009 \end{aligned}
\begin{aligned} K & \equiv A^b & \mod p \\ & \equiv 510^{456} & \mod 1009 \\ & \equiv 74 & \mod 1009 \end{aligned}

Alice と Bob は同じ共通鍵 K = 74 を持つことになります。

C 言語による DH 鍵共有の実装

以下に、C 言語での実装例を示します。先ほどの繰り返し二乗法の関数 mod_pow を使用しています。

#include <stdio.h>

int main() {
    int p = 1009;
    int g = 11;

    // private keys
    int a = 123;
    int b = 456;

    // public keys
    int A = mod_pow(g, a, p); // Alice public key
    int B = mod_pow(g, b, p); // Bob public key
    printf("Alice's public key: %d\n", A); // 510
    printf("Bob's public key: %d\n", B); // 185

    // Shared secret
    int s1 = mod_pow(B, a, p);
    int s2 = mod_pow(A, b, p);
    printf("Shared secret (Alice): %d\n", s1); // 74
    printf("Shared secret (Bob): %d\n", s2); // 74

    return 0;
}

計算量は、公開鍵の計算が O(\log a)O(\log b)、共通鍵の計算が O(\log b)O(\log a) であり、全体として O(\log a + \log b) となります。

離散対数の解法

離散対数問題は、与えられた整数 g, p, A に対して、g^a \equiv A \mod p を満たす a を求める問題です。この問題を解くための最も単純な方法は、a=1, \ldots, p-1 まで順に試し、g^a \mod p を計算しA と一致するかを確認することです。この方法は計算量が O(p) となります。

離散対数問題を解くためのより効率的なアルゴリズムとして、Baby-step Giant-step アルゴリズムがあります。このアルゴリズムは、計算量 O(\sqrt{p}) で離散対数を解くことができます。この記事では、Baby-step Giant-step アルゴリズムの手順と C 言語での実装例を示します。

Baby-step Giant-step アルゴリズム

Baby-step Giant-step アルゴリズムは、離散対数問題を解くための効率的な方法です。このアルゴリズムは、以下の手順で実行されます。

  1. 整数 q = \lceil \sqrt{p} \rceilを計算します。
  2. i = 0, ... q-1に対して、b = g^i \mod pを計算し、結果をハッシュテーブルに格納します。
  3. g^{-q} \mod p を計算します。
  4. j = 0, ... q-1に対して、A \cdot (g^{-q})^j \mod pを計算し、ハッシュテーブルに存在するか確認します。
  5. 一致する場合、x = i + j \cdot qが求める離散対数です。

最後のxが求める離散対数となるのは、A \cdot (g^{-q})^j \equiv g^i \mod p のときA \equiv g^{i + j \cdot q} \mod p となるからです。

手計算による確認

上記を手計算にて確認します。

まず、p=23, g=5 とします。通常、pは大きな素数を使用しますが、ここでは計算を簡単にするために小さな素数を使用します。Alice の秘密鍵a=8とします。このとき、Alice の公開鍵Aは次のように計算されます。

\begin{aligned} A & \equiv g^a &\mod 23 \\ & \equiv 5^8 &\mod 23 \\ & \equiv 16 &\mod 23 \end{aligned}

では、pgと公開鍵Aが与えられたとき、秘密鍵aを求めることを考えます。つまり以下を満たすaを求めます。

5^a \equiv 16 \mod 23

では、Baby-step Giant-step アルゴリズムを使用して解を求めます。まず、q = \lceil \sqrt{23} \rceil = 5 です。

次に、Baby-step を計算します。Baby-step では、i = 0, 1, 2, 3, 4 に対して g^i \mod p を計算し、結果をハッシュテーブルに格納します。

i g^i \mod p
0 1
1 5
2 2
3 10
4 8

次に、Giant-step を計算します。Giant-step では、j = 0, 1, 2, 3, 4 に対して A \cdot (g^{-q})^j \mod p を計算し、ハッシュテーブルに存在するか確認します。

j g^{-q \cdot j} \mod p A \cdot (g^{-q})^j \mod p Baby-step に存在するか
0 1 16 No
1 15 10 Yes (i=3)

j=1のとき、Baby-step の i=3 に対応します。したがって、求める離散対数は次のように計算されます。

x = i + j \cdot q = 3 + 1 \cdot 5 = 8

実際、g ^ x \mod p = 16であり、求める離散対数は x = 8 であることが確認できます。

C 言語による Baby-step Giant-step アルゴリズムの実装

以下に、C 言語での実装例を示します。

// find x such that A = g^x mod p
int discrete_log(int g, int A, int p) {
    int q = isqrt(p);

    // Baby-step
    HashMap *hashmap = newHashMap(q);
    int b = 1;
    insertToHashMap(hashmap, b, 0);
    for (int i = 1; i <= q; i++) {
        b = (b * g) % p;
        insertToHashMap(hashmap, b, i);
    }

    // Giant-step
    int g_inv = mod_pow(g, p-2, p);
    int g_inv_q = mod_pow(g_inv, q, p);

    int giant_step = A;
    int result = -1;

    for (int a=0; a<q; a++) {
        int b;
        if (getValueFromHashMap(hashmap, giant_step, &b) == HASHMAP_OK) {
            result = a * q + b;
            break;
        }
        giant_step = (giant_step * g_inv_q) % p;
    }

    freeHashMap(hashmap);
    return result;
}

g^{-1}の計算にはフェルマーの小定理を使用しています。計算量は、Baby-step の計算が O(\sqrt{p})、Giant-step の計算が O(\sqrt{p}) であり、全体として O(\sqrt{p}) となります。

HashMap の実装
#ifndef HASHMAP_H
#define HASHMAP_H

#include <stddef.h>

typedef enum { HASHMAP_OK, HASHMAP_ERROR } HashMapStatus;

typedef struct Entry Entry;
struct Entry {
  int key;
  int value;
  Entry *next;
};

typedef struct HashMap HashMap;
struct HashMap {
  size_t size;
  Entry **entries;
};

HashMap *newHashMap(int size);
HashMapStatus insertToHashMap(HashMap *hashmap, const int key, const int value);
HashMapStatus getValueFromHashMap(const HashMap *hashmap, const int key,
                                  int *value);
void freeHashMap(HashMap *hashmap);

#endif
#include "hashmap.h"

#include <limits.h>
#include <stdlib.h>

HashMap *newHashMap(int size) {
    HashMap *hashmap = malloc(sizeof(HashMap));
    hashmap->size = size;
    hashmap->entries = malloc(size * sizeof(Entry *));
    for (size_t i = 0; i < size; i++) {
        hashmap->entries[i] = NULL;
    }
    return hashmap;
}

int hash(int key, int size) {
    unsigned int ukey = (unsigned int)key;
    unsigned long long hash = ukey * 2654435761ULL;
    return (int)(hash % size);
}

HashMapStatus insertToHashMap(HashMap *hashmap, const int key, const int value) {
    int index = hash(key, hashmap->size);

    Entry *node = hashmap->entries[index];
    while (node) {
        if (node->key == key) {
            node->value = value;
            return HASHMAP_OK;
        }
        node = node->next;
    }

    Entry *new_node = malloc(sizeof(Entry));
    if (!new_node) return HASHMAP_ERROR;
    new_node->key = key;
    new_node->value = value;

    new_node->next = hashmap->entries[index];
    hashmap->entries[index] = new_node;
    return HASHMAP_OK;
}

HashMapStatus getValueFromHashMap(const HashMap *hashmap, const int key,
                                  int *value) {
    int index = hash(key, hashmap->size);

    Entry *node = hashmap->entries[index];
    while (node) {
        if (node->key == key) {
            *value = node->value;
            return HASHMAP_OK;
        }
        node = node->next;
    }

    return HASHMAP_ERROR;
}

void freeHashMap(HashMap *hashmap) {
    for (size_t i = 0; i < hashmap->size; i++) {
        Entry *node = hashmap->entries[i];
        while (node) {
            Entry *temp = node;
            node = node->next;
            free(temp);
        }
    }
    free(hashmap->entries);
    free(hashmap);
}

まとめ

この記事では、C 言語を使用して Diffie-Hellman 鍵共有と離散対数の解法を実装しました。Diffie-Hellman 鍵共有は、2 人の当事者が安全に共通の秘密鍵を生成するためのプロトコルです。離散対数問題は、公開鍵暗号の基礎となる数学的問題です。

今後は、有限体上の楕円曲線を使用した鍵共有や、より高度な離散対数の解法についても学んでいきたいと思います。

Discussion