晴耕雨読

working in the fields on fine days and reading books on rainy days

AES-NI 命令を用いた AES-128 暗号化の実装方法

前の記事で、実行環境の CPU が AES-NI に対応しているか確認する方法を紹介しました。 今回は実際に AES-NI (Advanced Encryption Standard New Instructions) 命令を使用して、C言語で AES-128 暗号化(ECBモード)を実装する方法を解説します。

AES-NI を利用することで、SIMD レジスタ(XMMレジスタ)を用いた高速な暗号化処理が可能になります。

必要なヘッダと型定義

AES-NI 命令を利用するには、Intel Intrinsics(組み込み関数)を提供する <wmmintrin.h> をインクルードします。また、128ビット幅のデータを扱うために __m128i 型を使用します。

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <wmmintrin.h> // AES-NI命令用のヘッダ

AES-128 鍵拡張 (Key Expansion) の実装

AES-128 では、128ビットのマスターキーから 11 個のラウンド鍵(合計 176 バイト)を生成する必要があります。AES-NI には、この鍵拡張を支援する _mm_aeskeygenassist_si128 命令が用意されています。

まず、鍵拡張の各ステップで行われるワードの回転や XOR 処理をまとめた補助関数を定義します。

// 鍵拡張の補助用マクロ関数
static inline __m128i AES_128_ASSIST(__m128i temp1, __m128i temp2) {
    __m128i temp3;
    temp2 = _mm_shuffle_epi32(temp2, 0xff);
    temp3 = _mm_slli_si128(temp1, 0x4);
    temp1 = _mm_xor_si128(temp1, temp3);
    temp3 = _mm_slli_si128(temp3, 0x4);
    temp1 = _mm_xor_si128(temp1, temp3);
    temp3 = _mm_slli_si128(temp3, 0x4);
    temp1 = _mm_xor_si128(temp1, temp3);
    temp1 = _mm_xor_si128(temp1, temp2);
    return temp1;
}

/**
 * AES-128 鍵拡張を実行する
 * @param userkey 16バイトのマスターキー
 * @param key_schedule 176バイトの出力バッファ
 */
void AES_128_Key_Expansion(const unsigned char *userkey, unsigned char *key_schedule) {
    __m128i temp1, temp2;
    __m128i *Key_Schedule = (__m128i*)key_schedule;

    // ラウンド 0 (マスターキーそのまま)
    temp1 = _mm_loadu_si128((__m128i*)userkey);
    Key_Schedule[0] = temp1;

    // ラウンド 1
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x1);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[1] = temp1;

    // ラウンド 2
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x2);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[2] = temp1;

    // ラウンド 3
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x4);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[3] = temp1;

    // ラウンド 4
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x8);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[4] = temp1;

    // ラウンド 5
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x10);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[5] = temp1;

    // ラウンド 6
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x20);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[6] = temp1;

    // ラウンド 7
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x40);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[7] = temp1;

    // ラウンド 8
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x80);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[8] = temp1;

    // ラウンド 9
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x1b);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[9] = temp1;

    // ラウンド 10
    temp2 = _mm_aeskeygenassist_si128(temp1, 0x36);
    temp1 = AES_128_ASSIST(temp1, temp2);
    Key_Schedule[10] = temp1;
}

AES 暗号化 (ECBモード) の実装

暗号化の本体では、_mm_aesenc_si128 (AES Encryption Round) と _mm_aesenclast_si128 (AES Last Encryption Round) を使用します。 これらの命令は、AES の「SubBytes」「ShiftRows」「MixColumns」「AddRoundKey」といった処理を 1 命令で実行します(※最終ラウンド用は MixColumns を含みません)。

/**
 * AES-ECB 暗号化を実行する
 * @param in 平文 (16バイトの倍数)
 * @param out 暗号文格納用
 * @param length バイト長
 * @param key 拡張済み鍵データ
 * @param number_of_rounds ラウンド数 (AES-128なら10)
 */
void AES_ECB_encrypt(const unsigned char *in,
                     unsigned char *out,
                     unsigned long length,
                     const unsigned char *key,
                     int number_of_rounds)
{
    __m128i tmp;
    int i, j;
    long block_count = length / 16;

    for (i = 0; i < block_count; i++) {
        // 入力データをSIMDレジスタにロード
        tmp = _mm_loadu_si128(&((__m128i*)in)[i]);

        // 最初のラウンド鍵を加算 (Initial Round)
        tmp = _mm_xor_si128(tmp, ((__m128i*)key)[0]);

        // 第1〜第(N-1)ラウンドの実行
        for (j = 1; j < number_of_rounds; j++) {
            tmp = _mm_aesenc_si128(tmp, ((__m128i*)key)[j]);
        }

        // 最終ラウンドの実行 (最終ラウンド専用命令を使用)
        tmp = _mm_aesenclast_si128(tmp, ((__m128i*)key)[j]);

        // 結果をメモリに書き出し
        _mm_storeu_si128(&((__m128i*)out)[i], tmp);
    }
}

テスト実行と結果の確認

NIST SP 800-38A に記載されているテストベクタを用いて、正しく暗号化できるか確認します。

int main() {
    // 128ビット (16バイト) のマスターキー (NIST SP 800-38A のテストベクタを使用)
    unsigned char key[16] = {
        0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
        0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
    };

    // 拡張された鍵を格納するバッファ (128ビット x 11 = 176バイト)
    unsigned char key_schedule[176];

    // 平文 (16バイトのブロック)
    unsigned char plaintext[16] = {
        0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96,
        0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a
    };

    // 暗号文を格納するバッファ
    unsigned char ciphertext[16];

    // 1. 鍵拡張を実行
    AES_128_Key_Expansion(key, key_schedule);

    // 2. 暗号化を実行(AES-128の場合、ラウンド数は10)
    AES_ECB_encrypt(plaintext, ciphertext, 16, (const char*)key_schedule, 10);

    // 3. 結果の表示
    printf("=== AES-128 ECB Encryption Test ===\n");
    printf("Plaintext : ");
    for (int i = 0; i < 16; i++) {
        printf("%02x", plaintext[i]);
    }
    printf("\n");

    printf("Key       : ");
    for (int i = 0; i < 16; i++) {
        printf("%02x", key[i]);
    }
    printf("\n");

    printf("Ciphertext: ");
    for (int i = 0; i < 16; i++) {
        printf("%02x", ciphertext[i]);
    }
    printf("\n");
    // 期待される暗号文: 3ad77bb40d7a3660a89ecaf32466ef97

    return 0;
}

コンパイルと実行

AES-NI 組み込み関数を使用しているため、コンパイル時に -maes オプションを付与する必要があります。

gcc -O2 -maes aes_ni_sample.c -o aes_ni_sample
./aes_ni_sample

実行結果:

=== AES-128 ECB Encryption Test ===
Plaintext : 6bc1bee22e409f96e93d7e117393172a
Key       : 2b7e151628aed2a6abf7158809cf4f3c
Ciphertext: 3ad77bb40d7a3660a89ecaf32466ef97

AES-NI 命令を直接使用することで、AES の複雑なラウンド処理を非常にシンプルなコードで実装でき、処理速度の高速化ができます。 実際の製品開発では OpenSSL などのライブラリを使用するのが一般的ですが、その内部で何が行われているかを知る上で、このような命令セットによる実装は非常に参考になります。

以上です。