HKDF

そもそものhkdfとはなにか、っていうのを理解しないと行けない。

hkdfとは、hmacをベースにしたシンプルなkdfである。

hmac、kdfの意味がわからないのでそれを調べる。

kdf

Key Derivation Functionのことで、日本語でいうと鍵導出関数となる。

鍵導出関数 (Key Derivation Function: KDF)は,与えられたパスワードから共通鍵暗号用の鍵を生成する関数である. 同一パスワードから異なる鍵が生成できるようにsalt Sと繰り返し数cの2つのパラメータを用いる.

saltは秘密の値ではなく、辞書攻撃等を防ぐためにある。

hmac

hmacはmacの一派らしい。

mac

改竄検知、認証のフローである。 手順を見るのがわかりやすい。

  1. 受信者、送信者の二者で共有鍵を共有する
  2. 送信者はメッセージと共有鍵の2つからMAC値を計算
  3. 送信者はメッセージとMAC値の両方を受信者に送る
  4. 受信者はメッセージを元にMAC値を計算する
  5. 受信者は送信されたMAC値を自分で計算したMAC値を比較する

で、hmacはこれをhashを使って実現する。手順を見ると雰囲気はわかる。

MAC(メッセージ認証コード)を、一方向ハッシュ関数(SHA256等)を用いて実現する方法。

  1. 共通鍵を一方向ハッシュ関数のブロック長より短い場合は、0パディングをする。
  2. 固定文字列ipad(バイト値 0x36 を 64回(ブロック長)繰り返した文字列)とパディングした鍵とのビット列のXORをとる
  3. 2で生成したバイト列をメッセージの先頭につける
  4. 3の結果を一方向ハッシュ関数を適用して、ハッシュ値を得る
  5. 固定文字列opad(バイト値 0x5C を 64 回(ブロック長)繰り返した文字列)と4で得られたビット列とのXORをとる
  6. 5で生成したビット列に4で生成されたハッシュをつける
  7. 6の結果を一方向ハッシュ関数に適用してハッシュ値を得る -> これがMAC値になる

hkdf

で、hkdfとはってなるわけだけれど、ここまでの理解で行くと、改ざん検知をしながら鍵導出を二者間で行う方法ってことになる。

hkdfでは、
1. PRKという疑似ランダムキーをikmとsaltをまぜたhmacによって抽出(extract)する。
2. PRKがOKM(output key material)を生成する。これは望んだ長さにできる。

Uses

hkdfには下記の2つのユースケースが有る。

  1. macの変数を変えることで、ランダムなソースから疑似乱数キーを作成する。
  2. 決められた長さのOKMを疑似乱数キーから生成する。

これらは、疑似乱数の偏りを防ぐのに使われるとともに、疑似乱数アルゴリズムを解析されるのを防ぐことに使われる。

このページで確認可能。

prkをexpandして、okmをextractしている。

Hashing type:    SHA-256  
Message:    hello  
Salt:        8e94ef805b93e683ff18  
Key bytes:    8 ( 64 -bit key)  
===================
PRK:    1e133888e9fed8f9ceb210f88af26fa8f62f4190dd230f6317bf9f61ee07a690

Key (Hex):        13485067e21af17c  

LNDのHKDF

HKDFはLNDにおいては下記コードで実施されている。

// mixKey is implements a basic HKDF-based key ratchet. This method is called
// with the result of each DH output generated during the handshake process.
// The first 32 bytes extract from the HKDF reader is the next chaining key,
// then latter 32 bytes become the temp secret key using within any future AEAD
// operations until another DH operation is performed.
func (s *symmetricState) mixKey(input []byte) {  
    var info []byte

    secret := input
    salt := s.chainingKey
    h := hkdf.New(sha256.New, secret, salt[:], info)

    // hkdf(ck, input, zero)
    // |
    // | \
    // |  \
    // ck  k
    h.Read(s.chainingKey[:])
    h.Read(s.tempKey[:])

    // cipher.k = temp_key
    s.InitializeKey(s.tempKey)
}

extractもexpandも見当たらない・・・。
そもそもnewは、

New returns a Reader, from which keys can be read, using the given hash, secret, salt and context info. Salt and info can be nil.

なので、hash、secret、saltとinfoを利用してキーのリーダーを作成する。

で、extractの項目を見ると、

Extract generates a pseudorandom key for use with Expand from an input secret and an optional independent salt.
Only use this function if you need to reuse the extracted key with multiple Expand invocations and different context values. Most common scenarios, including the generation of multiple keys, should use New instead.

Expandに利用する擬似ランダムキーを生成すると。ただし、抽出された鍵を再利用するときのみこの関数を利用し、普通のシナリオでは、複数の鍵を生成すするので、newを使うべきと。

用は、extractしてexpandするのを、newは一気にやってくれるわけだ。 なんとなくわかった。では、

    h.Read(c.salt[:])
    h.Read(nextKey[:])

これは一旦何なのか。 exampleで書かれているのは、このreadではなくて、io.readfullを使っているパターン。

// Generate three 128-bit derived keys.
hkdf := hkdf.New(hash, secret, salt, info)

var keys [][]byte  
for i := 0; i < 3; i++ {  
    key := make([]byte, 16)
    if _, err := io.ReadFull(hkdf, key); err != nil {
        panic(err)
    }
    keys = append(keys, key)
}

c.salt型は[]byteなのだけれど、sliceはgolangではpointerなので、その分だけ文字列が代入される。なるほど・・・

The first 32 bytes extract from the HKDF reader is the next chaining key, then latter 32 bytes become the temp secret key

理解した。長かった・・・