認証付き暗号AEGISを実装&解説 #Rust - Qiita

Linuxのソースコードを眺めているとCrypto系にAegisなるものを発見した。
ざっくり調べても特段日本語の情報が出てこなかったし、せっかくなのでソースコードと論文を読んでそれを備忘録として日本語でまとめておくことにした。
ソースコードはLinuxの物を載せるだけでも良かったが後学のために実際にRustで実装する。

この記事は、AEGISの使い方や等の実用面を想定したものではなく、あくまでAEGISのアルゴリズムや実装面に関して調査&解説したものなのでよろしく。

正式名称はA Fast Authenticated Encryption Algorithmで頭文字をとってAEGIS。
AEGIS-128,256,128Lの3バージョンあり、数字は鍵のビット数を表している。aegisはAEAD(認証付き暗号)の一種でAESのAEAD系であるAES-(gcm,ccm,ocb)より速く暗号処理が行える。RFCでは128がなくて128l,128×2,256とかになっていたので大体は128lでいいと思う。

処理内容はAES暗号のアルゴリズムを活用してAEADを行うものであり、現代のCPUが大体持っているAES-NI(AESの計算処理を高速で行えるようにCPUに備えられた命令セット)を使用できる上に並列計算できるように設計されているため高速で暗号化を行うことができる。

認証付きAES系(gcm,ccm,ocb)を使う場合は代わりにAEGISを使用することでセキュリティ強度を下げずに速度を向上させることができるので使用できる場合は使用することが推奨される。
(正直ここら辺の要約はClaudeとかに書かせたほうが速いし簡単だと思う)

論文をざっくり要約すると以上のような感じ。
Linuxの参照先の論文を読むと2013年に初版が書かれたらしいので思っているより古かった。とりあえず今回は最新のRFCではなく最初の論文を参考にして実装していく。AEADがわからない人は、wikiを見れば一瞬でわかると思うので割愛。

標準の関数(初期化、更新、後処理)はそれぞれで若干違うがほとんど一緒でよくある共通鍵暗号。(128Lは鍵、IV、タグの生成処理は128と一緒)
本記事ではAEGIS-128のみを乗せていく。

注意:エンディアンのことをなぁなぁでやってたらめちゃくちゃバグって大変だったので、きちんと把握しながら実装しましょう

実装は前述の通りRustで行う。
デバッグ用にclone連発してとても醜いものになってしまったのは忸怩たる思いだが、参考程度にしておいてほしい。

param

定番の構成パラメーター鍵、IV(init vector)、AD(associated data),const。

Param

#[derive(Debug,Default,Clone)]
struct Aegis{
  state : Vecu128>, //状態State
  iv : u128, //初期値Nonceの場合もある
  ad : Vecu128>,//ad認証時に使うオプション
  message: Vecu128>,//平文 ← 絶対保存するべきではないので注意マネしないように
  cipher_text : Vecu128>//暗号文
}
  // Fibonacci数列 mod 256の32バイト定数
  const FIBONACCI_CONSTANT: [u8; 32] = [
    0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0d,
    0x15, 0x22, 0x37, 0x59, 0x90, 0xe9, 0x79, 0x62,//上半分はconst0
    
    0xdb, 0x3d, 0x18, 0x55, 0x6d, 0xc2, 0x2f, 0xf1,
    0x20, 0x11, 0x31, 0x42, 0x73, 0xb5, 0x28, 0xdd //下半分はconst1
  ];

状態Stateが一番重要でIV、Adで初期化を行い。
以後それをUpdateしていくことで暗号化や複合、タグの作成を行う。
なので、本来であればこんな感じにするべきだと思うが、気力がない。

Aegis改

struct cipher_text{}
struct plane_text{}

#[derive(Default)]
struct State{
 iv : u128,
 ad : Vecu128>,
 c : cipher_text,
}

#[derive(Debug,Default,Clone)]
struct Aegis{
  state : State, //状態State
}

AESROUND

AESROUNDについては以前書いたAESの記事を読んでほしい。そこで書かれている関数処理がそのまま使用されている。

なおその記事でのコードを流用できるんじゃね?
とホクホク顔で使ってみたら、我ながら思いの外クソみたいなコードを書いていたので正直使わないほうがよかったがおかげで理解は深まったのでよしとする

追記:そのまま使用されていると書いたが実際に実装してみるとAES-NIのAESENC部分のことであり若干通常のラウンドと認識がずれている可能性があるので注意。具体的に言うと鍵の拡張を行う必要があるが、忘れて結果が合わず苦労した。もし実装する際にはそこは気を付けたほうが良い

AESROUND

//AESの一連の流れ
//鍵拡張 ←実はAES_ROUNDには含まれていないので注意
//sub_bytes
//shift_row
//mix_column
//add_round_key
//までをAES_ROUNDとする。

  fn aes_round(state: u128,key : u128)->u128{
    let mut state = state.to_be_bytes().to_vec();
    let aes = AES::new();
    let inverse = false;

    let u128_to_u32 =|x:u128|{
                    vec![
                    (x >>96 )as u32 ,
                    (x >> 64 ) as u32,
                    (x >>32 )as u32 ,
                     x as u32,                    ]
    };
    //鍵拡張 ←実はAES_ROUNDには含まれていないので注意
    //ここで拡張した鍵をラウンドで使用する
    //ラウンドに含まれていないので鍵の生成した段階で拡張鍵を生成できれば時短になる
    //今回はしない
    let key = u128_to_u32(key);
    let bit_type = aes_type::BitType::Aes128;
    let mode = aes_type::Mode::Ecb;

    let exkey : Key= Key::new(key.clone(),bit_type.clone(),mode.clone());
    let exkey = aes.key_expansion(exkey);
    //ここまで

    //sub_bytes
    //shift_row
    //mix_column
    //add_round_key
    //ここだけ見るとすごくすっきり
    state = sub_bytes(state,inverse);
    state = shift_row(state, inverse);
    state = mix_column(state, inverse);
    state = add_round_key(state, exkey.clone(), inverse);
    
    let state = u128::from_be_bytes(state.try_into().unwrap());
    state
  }

鍵の拡張処理だけ別処理にして入力で受け付ける形にしておけば、事前にAES用拡張鍵を生成できるのでそっちのほうが一回一回生成するより時短になるし何より美しい
以下参考コード

鍵入力のAESROUND


  fn aes_round(state: u128,exkey : &[u32])->u128{
    let mut state = state.to_be_bytes().to_vec();
    let aes = AES::new();
    let inverse = false;
    state = sub_bytes(state,inverse);
    state = shift_row(state, inverse);
    state = mix_column(state, inverse);
    state = add_round_key(state, exkey.clone(), inverse);
    
    let state = u128::from_be_bytes(state.try_into().unwrap());
    state
  }

stateUpdate

状態を保持しているSの更新関数。
状態Stateの大きさがAEGIS-128 = 80byte ,AEGIS-256 = 96byte,AEGIS-128L = 128byteなのでそれぞれラウンド数が違う。

実質的にはここが全てでAEGISの心臓部分、ほかの部分はほとんどおまけと言って良い
とは言ってもstateにmessageを混ぜるだけだからAESROUNDのほうが重要といえるかも

StateUpdate

fn state_update128(state : Vecu128>, message : u128) -> Vecu128>{
    let mut ret = state.clone();
    ret[0] = aes_round(state[4],state[0] ^ message);
    ret[1] = aes_round(state[0],state[1]);
    ret[2] = aes_round(state[1],state[2]);
    ret[3] = aes_round(state[2],state[3]);
    ret[4] = aes_round(state[3],state[4]);
    ret
}

見ての通り説明不要な美しさ。
 それぞれ独立しているのでSIMDで簡単に計算することができるし、AESROUNDをつかっているためAES-NIも使うことができる。これだけ簡単だと安全性が気になるが、そこはAESと同じでNonceとKeyでカバーできているので、AESと同じ位の安全性があると思う。(逆に言えばAESが突破されるとヤバそうではある)

 実はラウンド数(時間)と安全性は等価交換が基本だが、ことこれに関して言えば128l(一番ラウンド数が多い)のほうが速いらしい。理由はAES-NIのラウンド数と同じラウンド数のためハードウェアのメリットを最大限享受することができるため、だとか。つまり現状使うなら128l一択。

initialization & authenticated data

初期化処理。
ADは認証用のやつで入力していた場合初期化関数内の最後に処理が追加される。
内容自体は特段難しい部分もなく鍵と初期値を利用してXorかStateUpdateで計算するだけ。

init

  fn init(&mut self,key : u128) -> Vecu128>{
    //3.2.1
    let mut state : Vecu128> = self.state.clone();
    let mut m : Vecu128> = Vec::new();
    //フィボナッチ数列の最初から32バイトを分割
    let const0 =u128::from_be_bytes(Aegis::FIBONACCI_CONSTANT[0..16].try_into().unwrap());
    let const1 = u128::from_be_bytes(Aegis::FIBONACCI_CONSTANT[16..32].try_into().unwrap());

    //初期値には鍵、IV(nonce)、constを使用する
    //鍵とnonceを共有されていれば初期値は同一のものになる
    state[0] = key ^ self.iv;
    state[1] = const1;
    state[2] = const0;
    state[3] = key ^ const0;
    state[4] = key ^ const1;

    //3.2.2
    for i in 0..5 {
      m.push(key);
      m.push(key^self.iv);
    }
    //3.2.3
    for mi in m{
      state = state_update128(state, mi);
    }
    self.state = state.clone();
    
    //adがあったらここで処理を追加
    //無ければ何もしない
    if !self.ad.is_empty() {
      state = self.with_ad();
    }
    state
  }

ADがある場合、状態Stateを変更する処理が追加されるので言ってしまえばこれもNonceといえる。暗号文的な目線でいえば追加データを入れてればより暗号強度が上がる。

with_ad

fn with_ad(&mut self) -> Vecu128>{
    //3.3
    let ad = self.ad.clone();
    let mut state = self.state.clone();
    let adlen = ad.len();

    //adを状態に混ぜるだけ
    for i in 0..adlen{
      state = state_update128(state, ad[i]);
    }
    self.state = state.clone();
    state
}

encryption

暗号化処理。
アルゴリズム自体は非常に簡単なのであんまり説明は必要ないと思うが、平文を状態Stateに溶かすことで鍵のみでは状態を復元できないようにしている。つまり、暗号のNonceに平文を含めたものにしている形になっているので安全性を向上させている。

enc

  fn enc(&mut self,plane : Vecu128>)->Vecu128>{
    //3.4
    let messagelen = plane.len() ;
    self.message = plane.clone();
    let mut state = self.state.clone();
    let mut cipher_text : Vecu128> = Vec::new();

    //平文がない場合はなんもしない
    if messagelen == 0{
      return cipher_text;
    }
    //ブロック毎で暗号化
    for i in 0..messagelen{
      //ここがメイン
      let c = plane[i] ^ state[1] ^ state[4] ^ (state[2] & state[3]); 
      cipher_text.push(c);
      //平文の内容を状態に加えているので、鍵が同じでも平文で状態が変わる
      state = state_update128(state, plane[i]);
      
    }
    self.state = state.clone();
    self.cipher_text = cipher_text.clone();
    cipher_text

  }
  

finalize

認証用のタグの作成を行う関数。
状態StateにADやIVを用いて作成するため被りはほぼ発生しない。他のAEADではハッシュ関数を用いるがAEGISでは状態を再利用することで同様の機能を簡易で高速に出力している。(chacha20でもそうだったような?)

finalize

  fn finalize(&mut self) ->u128{
    //3.5.1
    let adlen = self.adlen.swap_bytes();
    let messagelen = self.messagelen.swap_bytes();
    let adlen_be = (adlen as u128)  64 ;
    let msglen_be = (messagelen as u128);
    let mut state = self.state.clone();

    let tmp = state[3] ^ (adlen_be | msglen_be);
    //3.5.2
    for i in 0..=6{
      state = state_update128(state, tmp);
    }
    //3.5.3
    let tag = state.iter().fold(0,|acc,part|acc ^ part);
    self.state = state;
    tag
  }

decryption and verification

複合処理と検証。
encでの処理はXORで処理されているので状態Stateを同じ状態にして暗号文と平文を入れ替えれば複合できる。
そして暗号処理前の状態Stateは鍵、IV(Nonce)、AD、constで構成されるため、秘匿情報を保持している場合は復元可能なので同じ状態にすることができる。
その後、検証は複合後の状態でタグの作成を行い一致していればOK

基本的に検証に失敗した平文やタグは絶対に何があっても渡してはいけないので覚えておいてほしい(詳しくは既知平文攻撃や選択暗号文攻撃で調べてほしい)

dec

  fn dec(&mut self,cipher : Vecu128>)-> (Vecu128>,u128){

    //今回はここでinit(&ad)を呼ぶ形でもよかった
    //実運用上はダメだが状態Stateを一致させられる
    
    let mut plane_text = Vec::new();
    let mut state = self.state.clone();
    //3.6.1
    let v = cipher.len();
    for i in 0..v{
      //暗号文と平文のブロックを入れ替えただけだが、xorの演算の特性上問題なく行える
      //状態Stateは既知の情報によって暗号化の際と完全に一致させられる
      let plane = cipher[i] ^ state[1] ^ state[4] ^ (state[2] & state[3]);
      plane_text.push(plane.clone());
      //複合された平文が同じ場合暗号化の場合と同じ状態Stateが生成できる
      state = state_update128(state, plane);
    }
    //tag
    //正しく生成できている場合はこのタグは暗号化した際のものと同様のものになる
    self.state = state.clone();
    let tag = self.finalize();
    (plane_text,tag)
  }

テストベクターは論文に書かれているものを使用する。
AEGIS-128に関してはあんまり詳細なテストベクター無かったので256か128lをおすすめする。
最新のRFCを見る限り128lが一般なのでそっちのほうがテストベクターも豊富だったしそっちにすればよかった。

A.1 Test vectors of AEGIS-128
associated data: 0 bits plaintext: 128 bits
K128 = 00000000000000000000000000000000
IV128 = 00000000000000000000000000000000
plaintext = 00000000000000000000000000000000
ciphertext = 951b050fa72b1a2fc16d2e1f01b07d7e
tag = a7d2a99773249542f422217ee888d5f1

associated data: 128 bits plaintext: 128 bits
K128 = 00000000000000000000000000000000
IV128 = 00000000000000000000000000000000
assoc. data = 00000000000000000000000000000000
plaintext = 00000000000000000000000000000000
ciphertext = 10b0dee65a97d751205c128a992473a1
tag = 46dcb9ee93c46cf13731d41b9646c131

associated data: 32 bits plaintext: 128 bits
K128 = 00010000000000000000000000000000
IV128 = 00000200000000000000000000000000
assoc. data = 00010203
plaintext = 00000000000000000000000000000000
ciphertext = 2b78f5c1618da39afbb2920f5dae02b0
tag = 74759cd0e19314650d6c635b563d80fd18

associated data: 64 bits plaintext: 256 bits
K128 = 10010000000000000000000000000000
IV128 = 10000200000000000000000000000000
assoc. data = 0001020304050607
plaintext = 000102030405060708090a0b0c0d0e0f
101112131415161718191a1b1c1d1e1f
ciphertext = e08ec10685d63c7364eca78ff6e1a1dd
fdfc15d5311a7f2988a0471a13973fd7
tag = 27e84b6c4cc46cb6ece8f1f3e4aa0e78

少しだけLinuxでのAegis-128実装を眺めてみたけど、実装部分は論文そのままなのでSIMDでの計算処理くらいしか特段面白い部分はなかった。C言語で見たい人はどうぞ。
https://github.com/torvalds/linux/blob/master/crypto/aegis128-core.c

「使用可能であればAEGISを積極的に使用していくとセキュリティと速度両方を向上させることができる」と色々なところで書かれていたが個人的には
AESのラウンドを減らしてタグの生成処理を足しただけでは?、
速度向上に関しては減らしたからじゃね?
と思っていたが間違いだった。

  • AESのラウンドを使用しているのでAES-NIが使用できる
  • 暗号化が単純な演算で可能なのでSIMD等並列計算が容易
  • ラウンドの回数がAESより少ない(おそらくこの回数でも問題ないという判断だと思う)

等、実際に調べてみると色々考えて設計されており非常に良いのではないかと考えを改めた。

2025年現在ではまだ使えない所が多いけど、TLS1.3とかで使用するRFCが出ているので今後に期待したい。

良い悪いとかは置いておいて正直既視感がすごい。
chachaもAESもaegisも何もかも幅は違えど同じ感じに見える。同じ血が流れている暗号達を触っている感覚がある。そういう点でも格子暗号系は一風変わっているのかもしれない。なんか家系図みたいでちょっと面白い。



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link