개발 이야기4분 읽기

길이를 고를 수 있는 해시 — SHAKE와 그 이웃들

SHA-3 계열에는 출력 길이가 정해지지 않은 함수가 있습니다. SHAKE·cSHAKE·KMAC·TupleHash가 각각 무엇을 푸는지 정리했습니다.

SHA-256의 출력은 32바이트입니다. 항상 그렇습니다. 48바이트가 필요하면 SHA-384로 바꾸거나, 두 번 돌려 이어 붙이거나, 파생 함수를 씁니다.

SHA-3 계열에는 그 제약이 없는 함수가 있습니다.

SHAKE는 원하는 만큼 뽑는다

  8바이트  325ea50b0911cedb
 16바이트  325ea50b0911cedbd77872fdfb66fee5
 32바이트  325ea50b0911cedbd77872fdfb66fee5e6bcc592be002b79839e5c2d59493ee8
 64바이트  325ea50b0911cedbd77872fdfb66fee5e6bcc592be002b79839e5c2d59493ee8
           6fe97fcaba11f1cce75cc1404a92099e2322bc8b378fc75d97b2a58484ae49a6

같은 입력 onuel에 길이만 바꿔 넣은 결과입니다. 이런 함수를 XOF(extendable-output function, 확장 출력 함수)라고 합니다.

눈에 띄는 성질이 하나 있습니다. 짧은 출력이 긴 출력의 접두사입니다. 8바이트 결과가 64바이트 결과의 앞 8바이트와 정확히 같습니다.

Keccak의 스펀지 구조 때문입니다. 입력을 상태에 흡수한 뒤 상태에서 바이트를 짜내는데, 짜내는 것을 멈추는 시점이 곧 출력 길이입니다. 상태는 같으므로 앞부분도 같습니다.

이 성질은 편하기도 하고 위험하기도 합니다. 16바이트 출력을 공개해 놓고 32바이트 출력을 비밀로 유지할 수는 없습니다. 앞 절반이 이미 나가 있습니다.

SHAKE128과 SHAKE256의 숫자는 출력 길이가 아니라 보안 강도입니다. SHAKE128에서 64바이트를 뽑아도 강도는 128비트입니다.

cSHAKE는 용도를 구분한다

한 시스템 안에서 같은 함수를 두 군데에 쓰면 문제가 생길 수 있습니다. 키 유도에 SHAKE를 쓰고 파일 지문에도 SHAKE를 쓰면, 어떤 입력이 우연히 같을 때 두 값이 같아집니다.

cSHAKE는 커스터마이제이션 문자열을 받습니다. "key-derivation""file-fingerprint"를 각각 넣으면 두 용도는 서로 절대 충돌하지 않습니다. 같은 입력이라도 다른 값이 나옵니다.

도메인 분리(domain separation)라고 부르는 장치이고, 직접 만들려면 접두사를 붙이는 방식으로 흉내 낼 수 있습니다. 다만 흉내에는 함정이 있습니다.

이어 붙이기는 모호하다

["ab", "c"]  →  "abc"  →  ba7816bf8f01cfea414140de5dae2223…
["a", "bc"]  →  "abc"  →  ba7816bf8f01cfea414140de5dae2223…
                          같은 값

서로 다른 두 입력이 같은 해시를 냅니다. 충돌을 찾은 것이 아니라, 이어 붙이는 순간 두 입력이 같은 바이트가 된 것입니다.

서명 대상을 만들 때 이 일이 일어납니다. 사용자명과 권한을 이어 붙여 서명하면, ("admi", "n")("admin", "")이 같은 값으로 서명됩니다.

길이를 함께 넣으면 갈라집니다.

["ab","c"] → "2:ab1:c" → 430fb1b4ac43316eca81fab27a1930ab…
["a","bc"] → "1:a2:bc" → 5310a58788781ab25d5ad7c3f8503582…

TupleHash가 표준으로 하는 일이 이것입니다. NIST SP 800-185는 left_encode라는 인코딩을 정의합니다 — 값 앞에 그 값의 바이트 수를 붙입니다.

left_encode(    0) = 0100
left_encode(    1) = 0101
left_encode(  255) = 01ff
left_encode(  256) = 020100
left_encode(65536) = 03010000

첫 바이트가 뒤따르는 길이 바이트의 개수이고, 그다음이 값입니다. 이 인코딩으로 각 조각을 감싸면 조각의 경계가 복원 가능해집니다.

KMAC은 HMAC이 필요 없다

HMAC은 해시를 두 번 겹쳐 씁니다.

HMAC(k, m) = H((k ⊕ opad) || H((k ⊕ ipad) || m))

이 구조는 SHA-1·SHA-2 같은 머클-담고르 해시의 길이 확장 공격을 막기 위한 것입니다. H(key || message)를 그냥 쓰면 키를 모르고도 메시지를 이어 붙일 수 있기 때문에, 안쪽 해시를 한 번 더 감쌉니다.

Keccak에는 길이 확장이 없습니다. 스펀지 구조라 내부 상태 전체가 출력되지 않습니다. 그래서 겹칠 이유도 없습니다.

KMAC은 cSHAKE 위에 키를 얹은 MAC입니다. 해시 호출이 한 번이고, 출력 길이를 고를 수 있으며, 커스터마이제이션 문자열도 그대로 받습니다.

어디에 쓰이는가

XOF는 결정론적으로 임의 길이 바이트가 필요한 자리에 들어갑니다. 후양자 서명 알고리즘 SPHINCS+와 ML-KEM(Kyber)이 내부에서 SHAKE를 씁니다 — 시드에서 필요한 만큼의 의사난수를 뽑는 용도입니다.

확인 순서

  1. 해시 출력을 잘라 쓰고 있다면 XOF로 바꿔 원하는 길이를 직접 뽑습니다
  2. 여러 조각을 이어 붙여 해시하거나 서명한다면 조각 길이를 함께 넣습니다. 구분자 하나로는 부족합니다 — 구분자가 데이터에 들어갈 수 있습니다
  3. 한 시스템에서 같은 해시를 여러 용도로 쓴다면 용도별 문자열로 갈라 놓습니다
  4. XOF의 짧은 출력은 긴 출력의 앞부분입니다. 앞부분을 공개했다면 뒷부분만으로 비밀을 유지할 수 없습니다
  5. SHAKE128의 128은 출력 길이가 아니라 보안 강도입니다

tools.onuel.devSHAKE에서 SHAKE128·SHAKE256의 출력 길이를 바꿔 가며 접두사 성질을 직접 확인할 수 있습니다. cSHAKE는 커스터마이제이션 문자열을, KMAC은 키를 받고, TupleHash["ab","c"]["a","bc"]를 넣으면 위와 달리 서로 다른 값이 나옵니다. 고정 길이 쪽과 비교하려면 SHA3-256이 같은 Keccak 계열입니다.

  • #해시
  • #SHA-3
  • #암호