pycrpytodome 패키지가 필요합니다. jupyter에서 !표시는 code를 실행하는 것이 아닌 prompt로 실행하라는 의미입니다
GCM(galois/count mode) 연산을 위한 함수와 padding에 필요한 함수를 아래와 같이 선언 합니다
!pip install pycryptodome
# Code generated by chatGPT3.5
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.strxor import strxor
import binascii
def gf_mult(x, y):
R = 0xe1 << 120
z = 0
v = int.from_bytes(x, byteorder='big')
w = int.from_bytes(y, byteorder='big')
for i in range(128):
if (w >> (127 - i)) & 1:
z ^= v
if v & 1:
v = (v >> 1) ^ R
else:
v >>= 1
return z.to_bytes(16, byteorder='big')
# Function to pad data to 16-byte blocks
def pad_to_16(data):
if len(data) % 16 != 0:
return data + b'\x00' * (16 - len(data) % 16)
return data
Defaulting to user installation because normal site-packages is not writeable Requirement already satisfied: pycryptodome in c:\users\jlee15\appdata\roaming\python\python312\site-packages (3.20.0)
[notice] A new release of pip is available: 24.0 -> 24.1 [notice] To update, run: python.exe -m pip install --upgrade pip
IEEE Std 802.1AE-2018, Table C-9.GCM-AES-128 Key and calculated ICV는 MACSec에서 payload에 대한 암호화 없이 GMAC만 사용하는 예제입니다. PDF 링크 걸어 둘게요 😉 IEEE Std 802.1AE-2018
key는 다음과 같이 주어져 있습니다.
# Key used for AES-128
key = bytes.fromhex('071B113B0CA743FECCCF3D051F737382')
cipher = AES.new(key, AES.MODE_ECB)
해당 예제에서는 GCM을 통한 Authetication tag(GMAC) 계산만 살펴보게되는데, 이 경우 GMAC 계산은 크게 3단계로 나누어 볼 수 있습니다.
- Initial counter 블럭 계산과 해당 블럭의 암호화
- GHASH 계산
- GHASH와 1에서 계산한 암호화블럭의 XOR 연산
아래 코드는 첫 번째 블럭의 암호화 값 E(K, Y[0])을 계산합니다.
H(Hash subkey)는 NIST800-38D 5.3에 따라 key 사이즈 만큼의 all-zero 값과 K:key에 AES_ECB 연산을 통해 유도합니다. H는 2단계 GHASH(H,A,C)에서 사용됩니다.
(A와 C는 각각 additional authenticated data:AAD, cipher text 입니다. 이후 자세히 살펴보도록 하죠)
MACSec에서 IV는 SCI(64bit) | PN(32bit)를 이어 만듭니다. # SCI(SecureChannel Identifier), PN(PacketNumber)
MACSec의 IV가 96bit가 된 것을 보니 NIST의 권고사항을 따라 잘 설계되었군요 😂
참고로 SCI는 MAC SA(SourceAddress) 48bit | Port ID 16bit를 이어서 만듭니다.
Y0은 IV를 참조해 만드는데 카운터 값 1을 패딩과 함께 넣어 128bit가 되도록 합니다.
#1. Encrypt the Initial Counter Block:
#1-1 Encrypt an all-zero block with given key for deriving H
zero_block = b'\x00' * 16
H = cipher.encrypt(zero_block)
print(f"H: {H.hex().upper()}")
#1-2 With initial counter block(Y0) formed from IV, produce E(K, Y0)
Y0 = bytes.fromhex('F0761E8DCD3D000176D457ED00000001')
print(f"Y[0]): {Y0.hex().upper()}")
E_Y0 = cipher.encrypt(Y0)
print(f"E(K, Y[0]): {E_Y0.hex().upper()}")
H: E4E01725D724C1215C7309AD34539257 Y[0]): F0761E8DCD3D000176D457ED00000001 E(K, Y[0]): FC25539100959B80FE3ABED435E54CAB
A(Additional Authenticated Data)의 경우 802.1AE Table C-9에 나와있는 예제 그대로 갖고 왔습니다.
A를 가볍게 이해하면, MACSec에서 authentication tag(GMAC)를 생성하기 위해 참조하는 block이라고 보면 됩니다.
MACSec에는 두 가지 mode가 있는데 integrity only mode 그리고 integrity and confidentiality mode 입니다. (정확한 용어인지는 모름..) payload 무결성만 관심있다면 전자를, payload 무결성 뿐만 아니라 암호화해서 보내고 싶다면 후자를 선택합니다.
모드에 따라 A를 계산하는 방식이 달라집니다.
Integrity only mode의 경우, A는 MACSec payload의 MAC DA | MAC SA | EtherType(88E5) | TCI/AN | SL | PN | SecureData 순으로 이어서 만듭니다. 참고하는 Table C-9은 integrity only mode입니다.
integrity and confidentiality mode의 경우, A는 MAC DA | MAC SA | EtherType(88E5) | TCI/AN | SL | PN 순으로 SecureData를 포함하지 않습니다.
두 방식의 차이가 있는 이유는 MACSec으로 생성된 payload의 tampering 가능여부에 차이가 있기 때문입니다.
전자의 경우 보내는 데이터(UserData)의 암호화를 하지 않았으니 GMAC에 모든 payload가 들어가야 무결성이 보장되는 데 반해,
후자의 경우 암호화시킨 UserData값에 대한 GMAC을 생성하므로 tampering 방지를 위한 GMAC 계산시 payload를 포함할 필요가 없기 때문입니다.
본론으로 돌아와서 전자의 방식으로 GHASH를 만드는 방법은 아래와 같습니다. 블럭은 키 사이즈에 상관없이 16byte 단위로 쪼개서 계산합니다.
GHASH(H,A,C)연산은 A를 블럭단위로 이전 Cipher에 XOR연산 후 galois multi 연산을 통해 cipher블럭을 계산합니다.
처음 cipher는 없기 때문에 all-zero block(X[0])으로 사용합니다.
맨 마지막에 생성된 cipher가 GHASH의 결과값이 됩니다.
#2. Compute GHASH
#2-1 Plain text(Additional authenticated data) in MACsec example
A = bytes.fromhex('E20106D7CD0DF0761E8DCD3D88E5400076D457ED08000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A0003')
len_A = (len(A) * 8).to_bytes(8, byteorder='big') # Lengths of A
A_padded = pad_to_16(A)
concatenated_data = A_padded + len_A
#2-2 Split concatenated data into 16-byte blocks
blocks = [concatenated_data[i:i+16] for i in range(0, len(concatenated_data), 16)]
if len(blocks[-1]) < 16:
blocks[-1] = blocks[-1] + b'\x00' * (16 - len(blocks[-1]))
#2-3 Compute GHASH
X = [b'\x00' * 16]# Initialize X[0] to zero
for i, block in enumerate(blocks): # Compute X[i]
temp = strxor(X[i], block)
X.append(gf_mult(temp, H))
print(f"X[{i+1}]: {X[i+1].hex().upper()}")
GHASH = X[-1] # Final GHASH value
print("GHASH(H,A,C):", GHASH.hex().upper())
X[1]: 8DAD4981E33493018BB8482F69E4478C X[2]: 5B0BFA3E67A3E080CB60EA3D523C734A X[3]: 051F8D267A68CF88748E56C5F64EF503 X[4]: 4187F1240DB1887F2A92DDAB8903A0F6 X[5]: C7D64941A90F02FA9FCDECC083B4B276 X[6]: F02428563BB7E67C378044C874498FF8 GHASH(H,A,C): F02428563BB7E67C378044C874498FF8
맨 마지막 GHASH의 결과값과 1번 과정에서 만들어둔 E(K, Y[0])의 결과값을 XOR 연산하면, MACSec에서 tag 혹은 ICV(Integrity check value)라고 불리는 authentication 최종 결과값을 얻을 수 있습니다.
#3 Combine GHASH and Encryption, for autentication tag T, T= E(K,Y0) ⊕ GHASH(H,A,C)
T = strxor(E_Y0, GHASH)
print("Authentication Tag (T):", T.hex().upper())
Authentication Tag (T): 0C017BC73B227DFCC9BAFA1C41ACC353