一覧NW-16-3 · WAN・トンネル・VPN

IPsec ESPとSAの更新

SPIと方向を照合し、認証・復号・SA更新で扱う値を確認します。

説明

IPsecのESPは、SAに対応した鍵と方式でパケットを保護します。外から見えるSPIやsequenceと、暗号化された内側の情報を分けて理解すると、観測できる範囲が分かります。

AES-GCMの固定試料を解析し、SAの方向とSPI、認証後の内側パケット、1ビット改変時の失敗を確認します。受信済み番号の扱い、新SAへの更新、追加長も調べます。

1外から確認できるESPの欄

outer-esp.pcapはIPv4の内側パケットをAES-GCMで保護したESPトンネルの試料です。

tcpdump -n -tt -vvv外側IP・SPI・sequence -r outer-esp.pcapESPトンネルの試料

外側IPのプロトコル50と、ESPのSPI0x1001、sequence1を確認できます。外側の送受信IPは配送に使うため見えますが、内側のIPとUDPは暗号化範囲に入っています。

2SPIとsequenceの8バイト

esp.binの先頭24バイトを表示します。

xxd -g 1 -l 24ESP先頭とIV esp.bin外側IPを除いたESP

最初の4バイト00001001がSPI、次の00000001がsequenceです。その後8バイトがこのAES-GCM方式の明示的IVで、16バイト目から暗号文が始まります。SPIは鍵そのものではなく、受信側がSAを選ぶための識別子です。

3方向ごとのSAを確認する

sa.jsonはこの試料のSAの対応です。更新準備中の新しいSAも含みます。

cat sa.jsonSPI・方向・宛先・状態

外向きSPI0x1001と内向きSPI0x1002は別のSAです。SAは方向を持つため、往復で一つのSPIを共有するとは限りません。相手、SPI、暗号方式、寿命やsequenceの状態を対応付けます。

4認証してから内側を取り出す

esp.mjsは公開の固定試験鍵で作った試料の復号処理です。GCMの認証を確認した後、paddingとtrailerを除きます。

node esp.mjsこの試料のAES-GCM処理 decrypt認証後に内側を復号

1000バイト、padding2、Next Header4、内側10.1.0.10→10.2.0.20で元データと一致します。トンネルモードでは内側IP全体を保護します。トランスポートモードとは暗号化範囲が違うため、モードも合わせて確認します。

5暗号文を1ビット変えた場合

同じ試料の暗号文を1ビット変え、元の認証タグで復号を試します。

node esp.mjs試料の認証処理 tamper1ビット改変を検査

認証が失敗し、平文は受け入れられません。この方式はSPIとsequenceを追加認証データとして扱い、暗号文とともに改変を検出します。外側IPヘッダー全部がこのESP認証の対象になるわけではありません。

6同じsequenceの再受信

暗号学的な認証が通る古いパケットでも、再送攻撃対策の状態によって受け入れない場合があります。この例は受信済み1と未受信2を比較します。

python3 -c受信済み番号を確認 'highest=1; seen={1}; print([(seq,seq not in seen and seq>highest-64) for seq in (1,2)])'認証とは別の再利用検出

受信済みの1はFalse、未受信の2はTrueです。ここでは64幅の確認窓の一部を示しています。実装では認証成功後に受信済み状態を更新し、未認証の番号では窓を進めません。

7更新後は新しいSAで番号を管理する

更新前後のESP先頭8バイトを比較します。

python3 -cSA更新前後のSPIと番号 'from pathlib import Path; import struct; print([(name,tuple(hex(v) for v in struct.unpack("!II",Path(name).read_bytes()[:8]))) for name in ("esp.bin","esp-new.bin")])'識別子が変わった新しいSA

SPI0x1001と0x2001で、どちらもsequence1です。新しい鍵とSAへ切り替えるため、新SAの番号を1から始められます。同じSAの番号やGCMのnonceを使い回してよいという意味ではありません。実際の更新には鍵管理と旧SAの切替手順が必要です。

8ESPで増えた長さ

内側、ESP全体、外側IP込みの長さを比較します。

python3 -c各層の長さを比較 'from pathlib import Path; sizes=[len(Path(n).read_bytes()) for n in ("inner.bin","esp.bin","outer-esp.bin")]; print(sizes,sizes[2]-sizes[0])'この試料の追加長

1000→1036→1056バイトで、追加は56バイトです。この内訳はESPヘッダー8、IV8、padding2、trailer2、タグ16、外側IP20です。アルゴリズムやpadding、NAT-T等の有無で追加長が変わるので、常に56バイトとは限りません。

まとめ

SPIは受信側がSAを選ぶ識別子で、方向や相手と合わせて確認します。

認証、再利用の検出、SA更新は異なる処理で、暗号化範囲と追加長も方式によって変わります。

操作履歴と解説を振り返れます。

シェル / PCAPと通信の記録

1
tcpdump -n -tt -vvv -r outer-esp.pcap
reading from file outer-esp.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto ESP (50), length 1056)
    192.0.2.1 > 198.51.100.20: ESP(spi=0x00001001,seq=0x1), length 1036
2
xxd -g 1 -l 24 esp.bin
00000000: 00 00 10 01 00 00 00 01 00 00 00 00 00 00 00 01  ................
00000010: 1f ae 03 b6 20 19 51 0c                          .... .Q.
3
cat sa.json
[
  {
    "direction": "out",
    "destination": "198.51.100.20",
    "spi": "0x1001",
    "algorithm": "AES-128-GCM",
    "state": "current"
  },
  {
    "direction": "in",
    "destination": "192.0.2.1",
    "spi": "0x1002",
    "algorithm": "AES-128-GCM",
    "state": "current"
  },
  {
    "direction": "out",
    "destination": "198.51.100.20",
    "spi": "0x2001",
    "algorithm": "AES-128-GCM",
    "state": "new"
  }
]
4
node esp.mjs decrypt
{
  "length": 1000,
  "padding": 2,
  "nextHeader": 4,
  "source": "10.1.0.10",
  "destination": "10.2.0.20",
  "matchesOriginal": true
}
5
node esp.mjs tamper
authentication failed; plaintext not accepted
6
python3 -c 'highest=1; seen={1}; print([(seq,seq not in seen and seq>highest-64) for seq in (1,2)])'
[(1, False), (2, True)]
7
python3 -c 'from pathlib import Path; import struct; print([(name,tuple(hex(v) for v in struct.unpack("!II",Path(name).read_bytes()[:8]))) for name in ("esp.bin","esp-new.bin")])'
[('esp.bin', ('0x1001', '0x1')), ('esp-new.bin', ('0x2001', '0x1'))]
8
python3 -c 'from pathlib import Path; sizes=[len(Path(n).read_bytes()) for n in ("inner.bin","esp.bin","outer-esp.bin")]; print(sizes,sizes[2]-sizes[0])'
[1000, 1036, 1056] 56

演習完了