암호화 알고리즘 내부 구조 (Cryptographic Algorithm Internals)

AES, SHA-256, RSA, ECDSA 등 리눅스 커널에서 사용되는 핵심 암호화 알고리즘의 내부 동작을 구현 수준으로 심층 분석합니다. 각 알고리즘의 수학적 기반, 라운드 변환, 키 스케줄, 보안 속성을 상세히 다룹니다.

사전 지식: 이 문서는 암호화 알고리즘의 내부 구조를 다룹니다. 리눅스 커널 Crypto API의 사용법(tfm, request, scatterlist 등)은 Linux Crypto Framework (Crypto API) 문서를 먼저 참고하세요.

대칭 암호 (Symmetric Ciphers)

대칭 암호(Symmetric Cipher)는 암호화와 복호화에 동일한 키를 사용하는 암호 체계입니다. 블록 암호(Block Cipher)는 고정 크기 블록 단위로 데이터를 처리하며, 스트림 암호(Stream Cipher)는 키스트림(Keystream)을 생성하여 평문과 XOR합니다. 리눅스 커널에서는 AES(블록 암호)와 ChaCha20(스트림 암호)이 가장 널리 사용됩니다.

AES (Advanced Encryption Standard) 내부 구조

AES(Advanced Encryption Standard)는 2001년 NIST가 FIPS 197로 표준화한 대칭 블록 암호입니다. 벨기에 암호학자 Joan Daemen과 Vincent Rijmen이 설계한 Rijndael(레인달) 알고리즘을 기반으로 하며, 128비트 블록 크기에 128/192/256비트 키를 지원합니다. AES-128은 10라운드, AES-192는 12라운드, AES-256은 14라운드를 수행합니다.

각 라운드는 4가지 변환으로 구성됩니다: SubBytes(바이트 치환), ShiftRows(행 시프트), MixColumns(열 혼합), AddRoundKey(라운드 키 XOR). 마지막 라운드에서는 MixColumns를 생략합니다. 이 4가지 변환의 조합이 혼돈(Confusion)과 확산(Diffusion)을 제공하여 암호학적 강도를 보장합니다.

상태 행렬(State Matrix)과 128비트 블록

AES는 128비트(16바이트) 입력을 4×4 바이트 행렬(상태 행렬, State Matrix)로 배치하여 처리합니다. 바이트는 열 우선(Column-Major) 순서로 배치됩니다. 입력 바이트 b0, b1, ..., b15는 다음과 같이 매핑됩니다.

AES 상태 행렬(State Matrix) — 열 우선 배치 입력 바이트 b₀ b₁ b₂ b₃ b₁₅ 상태 행렬 (4×4) Col 0 Col 1 Col 2 Col 3 b₀ b₄ b₈ b₁₂ b₁ b₅ b₉ b₁₃ b₂ b₆ b₁₀ b₁₄ b₃ b₇ b₁₁ b₁₅ S[r][c] = b[r + 4c]

상태 행렬의 각 바이트 S[r][c]는 유한체(Finite Field) GF(28) 위의 원소로 취급됩니다. GF(28)는 기약 다항식(Irreducible Polynomial) x8 + x4 + x3 + x + 1 (16진수 0x11B)을 사용합니다. 이 유한체에서의 덧셈은 XOR, 곱셈은 다항식 곱셈 후 기약 다항식으로 나눈 나머지입니다.

SubBytes — S-Box 치환

SubBytes 변환은 상태 행렬의 각 바이트를 S-Box(Substitution Box)를 통해 독립적으로 치환합니다. S-Box는 비선형(Non-linear) 변환으로, AES의 혼돈(Confusion) 속성을 제공합니다. 수학적으로 S-Box는 두 단계로 구성됩니다.

  1. GF(28) 역원(Multiplicative Inverse) — 입력 바이트의 유한체 역원을 구합니다. 0의 역원은 0으로 정의합니다.
  2. 아핀 변환(Affine Transformation) — 역원 결과에 GF(2) 위의 아핀 변환을 적용합니다.

아핀 변환은 다음과 같습니다: b'i = bi ⊕ b(i+4) mod 8 ⊕ b(i+5) mod 8 ⊕ b(i+6) mod 8 ⊕ b(i+7) mod 8 ⊕ ci (여기서 c = 0x63). 실제 구현에서는 256바이트 룩업 테이블(Lookup Table)로 미리 계산하여 사용합니다.

SubBytes — 각 바이트를 S-Box로 독립 치환 0x53 입력 바이트 행: 5 열: 3 S-Box [256 바이트] GF(2⁸) 역원 + 아핀 변환 0xED 출력 바이트 S-Box 발췌: S[0x00]=0x63, S[0x01]=0x7C S[0x53]=0xED, S[0xFF]=0x16 모든 16바이트에 대해 독립적으로 수행 — 병렬화 가능

S-Box의 비선형성은 선형 공격(Linear Cryptanalysis)과 차분 공격(Differential Cryptanalysis)에 대한 저항성의 핵심입니다. AES S-Box는 가능한 최대 비선형성(Maximum Non-linearity)에 가까운 값을 가집니다.

ShiftRows — 행 순환 시프트

ShiftRows는 상태 행렬의 각 행을 왼쪽으로 순환 시프트합니다. 행 0은 시프트하지 않고, 행 1은 1바이트, 행 2는 2바이트, 행 3은 3바이트 왼쪽으로 시프트합니다. 이 변환은 열 간 바이트 확산(Diffusion)을 제공하여, SubBytes에서 생긴 변화가 여러 열로 퍼지게 합니다.

ShiftRows — 행별 순환 시프트 변환 전 s₀₀ s₀₁ s₀₂ s₀₃ s₁₀ s₁₁ s₁₂ s₁₃ s₂₀ s₂₁ s₂₂ s₂₃ s₃₀ s₃₁ s₃₂ s₃₃ ← 0 시프트 ← 1 시프트 ← 2 시프트 ← 3 시프트 변환 후 s₀₀ s₀₁ s₀₂ s₀₃ s₁₁ s₁₂ s₁₃ s₁₀ s₂₂ s₂₃ s₂₀ s₂₁ s₃₃ s₃₀ s₃₁ s₃₂

MixColumns — 열 혼합 (GF(28) 곱셈)

MixColumns는 상태 행렬의 각 열을 GF(28) 위의 고정 다항식과 곱하여 변환합니다. 이 변환은 한 열 내의 4바이트를 서로 혼합하여 확산(Diffusion) 속성을 제공합니다. 곱셈에 사용되는 고정 MDS(Maximum Distance Separable) 행렬은 다음과 같습니다.

┌           ┐   ┌           ┐   ┌           ┐
│ s'₀  │   │ 02 03 01 01 │   │ s₀  │
│ s'₁  │ = │ 01 02 03 01 │ × │ s₁  │   (GF(2⁸) 위의 행렬 곱셈)
│ s'₂  │   │ 01 01 02 03 │   │ s₂  │
│ s'₃  │   │ 03 01 01 02 │   │ s₃  │
└           ┘   └           ┘   └           ┘

GF(28) 위의 곱셈에서 {02}를 곱하는 연산을 xtime이라 합니다. xtime(a)는 a를 1비트 왼쪽 시프트한 후, 최상위 비트가 1이었다면 기약 다항식 0x1B를 XOR합니다. {03}을 곱하는 것은 xtime(a) ⊕ a와 동일합니다.

/* GF(2^8)에서 {02} 곱셈: xtime */
static inline uint8_t xtime(uint8_t a)
{
    return (a << 1) ^ ((a & 0x80) ? 0x1B : 0x00);
}

/* MixColumns: 한 열(4바이트) 변환 */
void mix_column(uint8_t *col)
{
    uint8_t a = col[0], b = col[1], c = col[2], d = col[3];
    uint8_t t = a ^ b ^ c ^ d;  /* 공통 XOR 항 */

    col[0] = a ^ xtime(a ^ b) ^ t;  /* 2a + 3b + c + d */
    col[1] = b ^ xtime(b ^ c) ^ t;  /* a + 2b + 3c + d */
    col[2] = c ^ xtime(c ^ d) ^ t;  /* a + b + 2c + 3d */
    col[3] = d ^ xtime(d ^ a) ^ t;  /* 3a + b + c + 2d */
}
MixColumns — GF(2⁸) 행렬 곱셈으로 열 혼합 입력 열 s₀ s₁ s₂ s₃ × MDS 행렬 02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 = 출력 열 s'₀ s'₁ s'₂ s'₃ GF(2⁸) 곱셈 핵심: xtime ×02 = xtime(a) = (a≪1) ⊕ (MSB ? 0x1B : 0) ×03 = xtime(a) ⊕ a ×01 = a (항등원)

AddRoundKey — 라운드 키 XOR

AddRoundKey는 상태 행렬과 라운드 키(Round Key)를 바이트 단위로 XOR합니다. 라운드 키는 키 스케줄(Key Schedule)을 통해 원래 키에서 확장된 서브키입니다. 이 변환은 유일하게 키를 사용하는 단계로, 키 없이는 역변환이 불가능합니다.

/* AddRoundKey: 상태와 라운드 키 XOR */
void add_round_key(uint8_t state[4][4], const uint32_t *round_key)
{
    for (int c = 0; c < 4; c++) {
        uint32_t rk = round_key[c];
        state[0][c] ^= (rk >> 24) & 0xFF;
        state[1][c] ^= (rk >> 16) & 0xFF;
        state[2][c] ^= (rk >>  8) & 0xFF;
        state[3][c] ^= (rk      ) & 0xFF;
    }
}

키 스케줄(Key Schedule) — 128/192/256비트

AES 키 스케줄은 원래 암호화 키에서 각 라운드에 사용할 라운드 키를 생성합니다. AES-128은 4워드(128비트) 키에서 44워드(11개 라운드 키)를, AES-256은 8워드(256비트) 키에서 60워드(15개 라운드 키)를 생성합니다.

키 확장의 핵심 연산은 RotWord(1바이트 순환 시프트), SubWord(각 바이트를 S-Box로 치환), Rcon(라운드 상수 XOR)입니다.

/* AES-128 키 확장 (Nk=4, Nr=10, 총 44워드) */
void key_expansion_128(const uint8_t key[16], uint32_t w[44])
{
    /* 처음 4워드: 원래 키 복사 */
    for (int i = 0; i < 4; i++)
        w[i] = GET_U32_BE(key, 4 * i);

    for (int i = 4; i < 44; i++) {
        uint32_t temp = w[i - 1];

        if (i % 4 == 0) {
            /* RotWord: [a,b,c,d] → [b,c,d,a] */
            temp = (temp << 8) | (temp >> 24);
            /* SubWord: 각 바이트를 S-Box로 치환 */
            temp = sub_word(temp);
            /* Rcon XOR: 라운드 상수 */
            temp ^= rcon[i / 4 - 1];  /* rcon = {01,02,04,08,10,20,40,80,1B,36} */
        }

        w[i] = w[i - 4] ^ temp;
    }
}

/* AES-256 키 확장 (Nk=8): i%8==4일 때 추가 SubWord */
/* AES-192 키 확장 (Nk=6): 구조는 동일하되 i%6==0일 때 변환 */
AES-128 키 스케줄 — 44워드 생성 흐름 W[0] W[1] W[2] W[3] 원래 키 (128비트 = 4워드) RotWord SubWord ⊕ Rcon[i] W[4] W[5] W[7] W[i] = W[i-4] ⊕ W[i-1] (i%4 ≠ 0) i % 4 == 0일 때만 RotWord → SubWord → Rcon 적용

AES-256에서는 추가로 i % 8 == 4인 경우에도 SubWord를 적용합니다. 이는 256비트 키의 확산을 강화하기 위한 것입니다. Rcon(라운드 상수)은 GF(28)에서 {02}의 거듭제곱입니다: Rcon = {01, 02, 04, 08, 10, 20, 40, 80, 1B, 36}.

AES 전체 라운드 흐름

AES 암호화의 전체 흐름은 다음과 같습니다: 초기 AddRoundKey → (Nr-1)회 메인 라운드 → 마지막 라운드(MixColumns 생략). AES-128의 경우 총 10라운드를 수행합니다.

AES 암호화 전체 흐름 (AES-128: 10라운드) 평문(Plaintext) 128비트 AddRoundKey (K₀) ×(Nr-1) 반복 AES-128: 9회 SubBytes ShiftRows MixColumns AddRoundKey (K_r) 마지막 라운드 SubBytes ShiftRows AddRoundKey (K_Nr) MixColumns 없음! 암호문(Ciphertext) 128비트

복호화(Decryption) — 역변환 순서

AES 복호화는 각 변환의 역을 역순으로 적용합니다: InvShiftRows(오른쪽 시프트), InvSubBytes(역 S-Box), InvMixColumns(역 MDS 행렬), AddRoundKey(XOR은 자기 자신의 역).

역 MDS 행렬의 계수는 {0E, 0B, 0D, 09}로, 정방향보다 계산 비용이 높습니다. 동치 역 암호(Equivalent Inverse Cipher) 최적화를 사용하면, 라운드 키에 InvMixColumns를 미리 적용하여 암호화와 동일한 구조를 사용할 수 있습니다. 이 최적화는 AES-NI 하드웨어 명령어(AESIMC)로도 지원됩니다.

/* AES 복호화: 역변환 순서 */
void aes_decrypt(const uint8_t *ct, uint8_t *pt, const uint32_t *rk, int nr)
{
    uint8_t state[4][4];
    bytes_to_state(ct, state);

    add_round_key(state, rk + nr * 4);  /* 마지막 라운드 키부터 역순 */

    for (int r = nr - 1; r >= 1; r--) {
        inv_shift_rows(state);   /* InvShiftRows: 오른쪽 순환 시프트 */
        inv_sub_bytes(state);    /* InvSubBytes: 역 S-Box 치환 */
        add_round_key(state, rk + r * 4);
        inv_mix_columns(state);  /* InvMixColumns: 역 MDS 행렬 */
    }
    /* 마지막 라운드 (InvMixColumns 없음) */
    inv_shift_rows(state);
    inv_sub_bytes(state);
    add_round_key(state, rk);

    state_to_bytes(state, pt);
}

/* InvMixColumns: 역 MDS 행렬 곱셈 */
/* 계수: {0E, 0B, 0D, 09} — xtime 반복 조합으로 계산 */
/* 0E = xtime(xtime(xtime(a))) ^ xtime(xtime(a)) ^ xtime(a) */

보안 속성과 알려진 공격

AES는 2001년 표준화 이후 실질적으로 깨진 적이 없습니다. 알려진 최선의 공격은 다음과 같습니다.

공격대상복잡도실용성
BicliqueAES-1282126.1비실용적 (전수 조사와 거의 동일)
BicliqueAES-2562254.4비실용적
Related-KeyAES-256299.5관련 키 조건 비현실적
캐시 타이밍전체로컬 접근 필요실용적 — AES-NI로 방어
사이드 채널 공격 방어: 소프트웨어 AES 구현은 S-Box 테이블 룩업 시 캐시 타이밍 공격에 취약합니다. 리눅스 커널은 AES-NI(x86) 또는 ARM CE 하드웨어 명령어를 사용하여 상수 시간(Constant-Time) 실행을 보장합니다. 상세 내용은 Crypto API의 AES-NI 섹션을 참고하세요.

블록 암호 운용 모드 (Block Cipher Modes of Operation)

블록 암호는 고정 크기(AES: 128비트) 블록만 처리할 수 있으므로, 임의 길이 데이터를 암호화하려면 운용 모드(Mode of Operation)가 필요합니다. 운용 모드는 블록 간의 관계를 정의하며, 각 모드는 병렬성, 에러 전파, 패딩 필요성 등에서 서로 다른 특성을 가집니다.

ECB (Electronic Codebook) — 사용 금지 모드

ECB 모드는 각 블록을 독립적으로 암호화합니다: Ci = EK(Pi). 동일한 평문 블록은 동일한 암호문을 생성하므로, 패턴이 보존됩니다. ECB로 이미지를 암호화하면 원본의 윤곽이 그대로 드러나는 것이 대표적인 예입니다. ECB는 단일 블록 암호화(예: 키 래핑) 이외의 용도로 사용해서는 안 됩니다.

CBC (Cipher Block Chaining) 모드

CBC 모드는 이전 암호문 블록을 다음 평문 블록과 XOR한 후 암호화합니다. 첫 블록에는 초기화 벡터(IV, Initialization Vector)를 사용합니다.

암호화는 블록 간 의존성으로 직렬화되지만, 복호화는 병렬 수행이 가능합니다. 블록 크기의 배수가 아닌 데이터는 패딩(PKCS#7)이 필요합니다.

CBC 모드 암호화 — 블록 체이닝 IV P₁ E_K C₁ P₂ E_K C₂ P₃ E_K C₃ C_{i-1}이 다음 블록의 XOR 입력으로 체이닝 — 암호화는 직렬, 복호화는 병렬 가능
패딩 오라클 공격(Padding Oracle Attack): CBC 모드에서 복호화 시 패딩 오류를 구별 가능한 응답으로 반환하면, 공격자가 이를 오라클로 활용하여 평문을 복원할 수 있습니다. 이 공격을 방지하려면 MAC-then-Encrypt 대신 Encrypt-then-MAC을 사용하거나, AEAD 모드(GCM, ChaCha20-Poly1305)를 사용하세요.

CTR (Counter) 모드

CTR 모드는 블록 암호를 스트림 암호처럼 사용합니다. Nonce와 카운터를 결합한 값을 암호화하여 키스트림을 생성하고, 이를 평문과 XOR합니다.

모든 블록이 독립적이므로 완전한 병렬 처리가 가능하며, 패딩이 불필요합니다. 그러나 동일한 키-Nonce 조합의 재사용은 치명적입니다 — 두 암호문을 XOR하면 두 평문의 XOR이 노출됩니다.

CTR 모드 — 완전 병렬 암호화 Nonce ∥ Counter₁ E_K P₁ C₁ Nonce ∥ Counter₂ E_K P₂ C₂ Nonce ∥ Counter₃ E_K P₃ C₃ 모든 블록 독립 ∥ 병렬

XTS (XEX-based Tweaked CodeBook) 모드

XTS 모드는 디스크 암호화에 특화된 모드로, IEEE P1619 표준입니다. 두 개의 키(K1: 데이터 암호화, K2: 트윅 암호화)를 사용하며, 섹터 번호를 트윅(Tweak)으로 활용합니다.

트윅은 동일한 평문이 다른 위치에서 다른 암호문을 생성하게 하며, 디스크의 각 섹터가 독립적으로 접근 가능합니다. 리눅스 dm-crypt와 fscrypt에서 xts(aes)로 사용됩니다.

XTS 모드 — 트윅 기반 디스크 암호화 섹터 번호 (i) E_K₂ T = E_K₂(i) T·α⁰ T·α¹ T·α² P₀ T·α⁰ E_K₁ T·α⁰ C₀ P₁ E_K₁ C₁ P₂ E_K₁ C₂ K₂: 트윅 키 K₁: 데이터 키 α = x in GF(2¹²⁸)

XTS의 핵심은 트윅 값이 블록 위치에 따라 변하므로, 동일한 평문이라도 디스크의 다른 위치에서는 완전히 다른 암호문을 생성한다는 점입니다. α 곱셈은 GF(2128)에서의 {02} 곱셈으로, 1비트 시프트와 조건부 XOR(0x87)로 효율적으로 계산됩니다. 마지막 불완전 블록은 CTS(Ciphertext Stealing)로 처리합니다.

/* XTS 트윅 업데이트: T = T × α in GF(2^128) */
static void xts_tweak_next(uint8_t *T)
{
    int carry = 0;
    for (int i = 0; i < 16; i++) {
        int next_carry = (T[i] >> 7) & 1;
        T[i] = (T[i] << 1) | carry;
        carry = next_carry;
    }
    if (carry)
        T[0] ^= 0x87;  /* GF(2^128) 기약 다항식: x^128 + x^7 + x^2 + x + 1 */
}

운용 모드 비교

블록 암호 운용 모드 비교
모드 암호화 병렬 복호화 병렬 패딩 필요 에러 전파 인증 주요 용도
ECBOOO해당 블록만X사용 금지
CBCXOO2블록X레거시 TLS, IPsec
CTROOX해당 비트만XGCM 내부
XTSOOCTS해당 블록만X디스크 암호화
GCMOOX전체 무효화OTLS, IPsec, kTLS

ChaCha20 스트림 암호 내부 구조

ChaCha20은 Daniel Bernstein이 설계한 스트림 암호(Stream Cipher)로, ARX(Add-Rotate-XOR) 연산만 사용합니다. S-Box가 없어 타이밍 공격에 본질적으로 안전하며, AES-NI가 없는 플랫폼에서도 AES보다 빠릅니다. WireGuard, TLS 1.3에서 ChaCha20-Poly1305 AEAD로 널리 사용됩니다.

초기 상태 행렬 (4×4 워드)

ChaCha20의 상태는 16개의 32비트 워드로 구성된 4×4 행렬입니다.

상태 행렬 (512비트 = 16 × 32비트):
┌──────────┬──────────┬──────────┬──────────┐
│ "expa"   │ "nd 3"   │ "2-by"   │ "te k"   │  ← 상수 (0x61707865, 0x3320646E, ...)
├──────────┼──────────┼──────────┼──────────┤
│ Key[0]   │ Key[1]   │ Key[2]   │ Key[3]   │  ← 키 전반부 (128비트)
├──────────┼──────────┼──────────┼──────────┤
│ Key[4]   │ Key[5]   │ Key[6]   │ Key[7]   │  ← 키 후반부 (128비트)
├──────────┼──────────┼──────────┼──────────┤
│ Counter  │ Nonce[0] │ Nonce[1] │ Nonce[2] │  ← 카운터(32비트) + Nonce(96비트)
└──────────┴──────────┴──────────┴──────────┘

"expand 32-byte k" ASCII 상수(리틀 엔디안)는 아무 의미 없는(Nothing-up-my-sleeve) 숫자로, 설계자가 의도적으로 약점을 심지 않았음을 보여줍니다.

쿼터 라운드(Quarter Round) 함수

ChaCha20의 핵심은 쿼터 라운드(QR) 함수입니다. 4개의 32비트 워드 (a, b, c, d)에 대해 ARX 연산을 수행합니다.

/* ChaCha20 쿼터 라운드 */
#define QR(a, b, c, d) do { \
    a += b; d ^= a; d = ROTL32(d, 16); \
    c += d; b ^= c; b = ROTL32(b, 12); \
    a += b; d ^= a; d = ROTL32(d,  8); \
    c += d; b ^= c; b = ROTL32(b,  7); \
} while (0)
ChaCha20 쿼터 라운드 — ARX 연산 4단계 단계 1 a += b; d ^= a; d ⊲ 16 단계 2 c += d; b ^= c; b ⊲ 12 단계 3 a += b; d ^= a; d ⊲ 8 단계 4 c += d; b ^= c; b ⊲ 7 ARX = Add(모듈러 덧셈) + Rotate(비트 회전) + XOR S-Box 없이 산술 연산만 사용 → 상수 시간 실행, 타이밍 공격 면역 더블 라운드(Double Round) = 컬럼 라운드 + 대각선 라운드 컬럼 라운드 (Column Round) QR(0, 4, 8, 12) QR(1, 5, 9, 13) QR(2, 6, 10, 14) QR(3, 7, 11, 15) 대각선 라운드 (Diagonal Round) QR(0, 5, 10, 15) QR(1, 6, 11, 12) QR(2, 7, 8, 13) QR(3, 4, 9, 14) 20라운드 = 10 더블 라운드. 최종 상태 = 초기 상태 + 작업 상태 (워드별 덧셈)

키스트림 생성과 암호화

/* ChaCha20 블록 함수: 512비트 키스트림 블록 생성 */
void chacha20_block(const uint32_t input[16], uint32_t output[16])
{
    uint32_t x[16];
    memcpy(x, input, sizeof(x));

    /* 20라운드 = 10 더블 라운드 */
    for (int i = 0; i < 10; i++) {
        /* 컬럼 라운드 */
        QR(x[0], x[4], x[ 8], x[12]);
        QR(x[1], x[5], x[ 9], x[13]);
        QR(x[2], x[6], x[10], x[14]);
        QR(x[3], x[7], x[11], x[15]);
        /* 대각선 라운드 */
        QR(x[0], x[5], x[10], x[15]);
        QR(x[1], x[6], x[11], x[12]);
        QR(x[2], x[7], x[ 8], x[13]);
        QR(x[3], x[4], x[ 9], x[14]);
    }

    /* 최종: 초기 상태와 워드별 덧셈 */
    for (int i = 0; i < 16; i++)
        output[i] = x[i] + input[i];  /* 모듈러 2^32 덧셈 */
}

/* 암호화: 키스트림 ⊕ 평문 */
/* 카운터를 1씩 증가시키며 다음 64바이트 블록 생성 */

ChaCha20 vs AES 비교

특성AES-256ChaCha20
키 크기128/192/256비트256비트
블록 크기128비트512비트 (키스트림)
핵심 연산S-Box + GF 곱셈ARX (Add, Rotate, XOR)
HW 가속AES-NI, ARM CENEON, AVX2 (SIMD)
SW 타이밍 안전테이블 룩업 취약본질적으로 상수 시간
AES-NI 있을 때빠름 (~10 GB/s)보통 (~3 GB/s)
AES-NI 없을 때느림 (~300 MB/s)빠름 (~1.5 GB/s)

해시 함수 (Hash Functions)

암호학적 해시 함수(Cryptographic Hash Function)는 임의 길이 입력에서 고정 길이 출력(다이제스트, Digest)을 생성하는 일방향 함수입니다. 역상 저항성(Preimage Resistance), 제2 역상 저항성(Second Preimage Resistance), 충돌 저항성(Collision Resistance)의 세 가지 보안 속성을 만족해야 합니다.

SHA-256 내부 구조

SHA-256은 SHA-2 계열에 속하며, NIST FIPS 180-4에 표준화되어 있습니다. 32비트 워드 기반으로 동작하며, 256비트(32바이트) 다이제스트를 생성합니다. 리눅스 커널에서 IMA, 모듈 서명, dm-verity, fscrypt 등 광범위하게 사용됩니다.

Merkle-Damgård 구성

SHA-256은 Merkle-Damgård 구성을 사용합니다. 메시지를 512비트 블록으로 분할하고, 각 블록을 압축 함수(Compression Function)에 순차적으로 투입합니다. 이전 블록의 출력(256비트 체이닝 값)이 다음 블록의 입력이 됩니다.

SHA-256 Merkle-Damgård 구성 — 반복 압축 IV (H₀) 압축 함수 f M₀ (512b) 압축 함수 f M₁ (512b) 압축 함수 f M_{n-1} (512b) 해시값 (256비트) H_i = f(H_{i-1}, M_i) — 체이닝 값(256비트)이 다음 블록의 입력으로 전달 IV = H₀ = {6a09e667, bb67ae85, 3c6ef372, a54ff53a, 510e527f, 9b05688c, 1f83d9ab, 5be0cd19}

전처리 — 패딩과 메시지 스케줄

패딩: 메시지 끝에 1비트(0x80)를 추가하고, 메시지 길이가 512의 배수 - 64비트가 될 때까지 0을 채운 후, 원래 메시지 길이를 64비트 빅 엔디안으로 추가합니다.

메시지 스케줄(Message Schedule): 각 512비트 블록에서 64개의 32비트 워드 W[0..63]을 생성합니다.

/* 메시지 스케줄 확장 */
/* W[0..15]: 입력 블록에서 직접 추출 (빅 엔디안) */
for (int t = 16; t < 64; t++) {
    uint32_t s0 = ROTR(W[t-15], 7) ^ ROTR(W[t-15], 18) ^ (W[t-15] >> 3);   /* σ₀ */
    uint32_t s1 = ROTR(W[t-2], 17) ^ ROTR(W[t-2], 19)  ^ (W[t-2] >> 10);   /* σ₁ */
    W[t] = W[t-16] + s0 + W[t-7] + s1;
}
SHA-256 메시지 스케줄 — W[0..63] 확장 W[0] W[1] W[2] … W[14] W[15] ← 입력 블록(512비트)에서 직접 추출 W[16..63]: 이전 워드로부터 확장 (t = 16..63) W[t-16] + σ₀(W[t-15]): ROTR7⊕ROTR18⊕SHR3 + W[t-7] + σ₁(W[t-2]): ROTR17⊕ROTR19⊕SHR10 = W[t] σ₀, σ₁: 소문자 시그마 — ROTR(Rotate Right)과 SHR(Shift Right) 조합으로 비선형 확산

압축 함수(Compression Function) — 64라운드

압축 함수는 8개의 32비트 작업 변수 a, b, c, d, e, f, g, h를 이전 해시값(Hi-1)으로 초기화한 후, 64라운드에 걸쳐 업데이트합니다.

/* SHA-256 라운드 함수 핵심 연산 */
#define Ch(e,f,g)   ((e & f) ^ (~e & g))
#define Maj(a,b,c)  ((a & b) ^ (a & c) ^ (b & c))
#define Sigma0(a)   (ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22))   /* Σ₀ */
#define Sigma1(e)   (ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25))   /* Σ₁ */

/* 한 라운드 (t = 0..63) */
uint32_t T1 = h + Sigma1(e) + Ch(e, f, g) + K[t] + W[t];
uint32_t T2 = Sigma0(a) + Maj(a, b, c);

h = g;
g = f;
f = e;
e = d + T1;    /* d에 T1을 더해서 새로운 e */
d = c;
c = b;
b = a;
a = T1 + T2;   /* T1 + T2가 새로운 a */

/* 64라운드 완료 후: H_i[j] = H_{i-1}[j] + 작업변수[j] */
SHA-256 라운드 함수 — 작업 변수 업데이트 a b c d e f g h Σ₁(e) + Ch(e,f,g) + K[t] + W[t] T₁ Σ₀(a) + Maj(a,b,c) T₂ 다음 라운드 변수: T₁+T₂ → a' a → b' b → c' c → d' d+T₁ → e' e → f' f → g' g → h' Ch(e,f,g) = (e AND f) XOR (NOT e AND g) — 조건 선택 (e가 1이면 f, 0이면 g) Maj(a,b,c) = (a AND b) XOR (a AND c) XOR (b AND c) — 다수결

초기값과 라운드 상수

SHA-256의 초기 해시값 H0은 처음 8개 소수(2, 3, 5, 7, 11, 13, 17, 19)의 제곱근 소수 부분에서 유도됩니다. 64개의 라운드 상수 K[t]는 처음 64개 소수의 세제곱근 소수 부분입니다.

/* SHA-256 초기 해시값 (처음 8개 소수의 √ 소수 부분) */
uint32_t H[8] = {
    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
    0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
};

/* SHA-256 라운드 상수 (처음 64개 소수의 ∛ 소수 부분) */
uint32_t K[64] = {
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
    0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    /* ... 나머지 56개 ... */
};

충돌 저항성과 길이 확장 공격

SHA-256의 충돌 저항성은 생일 공격(Birthday Attack)에 의해 2128으로 바운드됩니다. 현재까지 이론적으로나 실용적으로 SHA-256 충돌은 발견되지 않았습니다.

길이 확장 공격(Length Extension Attack): Merkle-Damgård 해시의 구조적 약점으로, H(key ∥ msg)를 알면 key를 모르고도 H(key ∥ msg ∥ padding ∥ extra)를 계산할 수 있습니다. 따라서 H(key ∥ msg)를 MAC으로 사용해서는 안 됩니다. 대신 HMAC을 사용하세요.

SHA-512와 SHA-256 차이점

SHA-512는 SHA-256과 동일한 구조를 사용하지만, 64비트 워드 기반으로 동작합니다.

속성SHA-256SHA-512
워드 크기32비트64비트
블록 크기512비트1024비트
다이제스트 크기256비트512비트
라운드 수6480
회전/시프트 상수2,13,22 / 6,11,2528,34,39 / 14,18,41
64비트 CPU 성능보통빠름 (네이티브 64비트 연산)
32비트 CPU 성능빠름느림 (64비트 에뮬레이션)

SHA-384는 SHA-512의 절삭(Truncated) 변형으로, 다른 초기값을 사용하고 출력을 384비트로 자릅니다. SHA-512/256은 SHA-512 기반이지만 256비트 출력을 제공하며, SHA-256보다 길이 확장 공격에 강합니다.

HMAC (Hash-based Message Authentication Code) 구성

HMAC은 해시 함수를 기반으로 메시지 인증 코드(MAC)를 생성하는 구성입니다. RFC 2104에 정의되며, 리눅스 커널에서 kTLS, IPsec, DRBG, fscrypt 키 유도 등에 광범위하게 사용됩니다.

ipad/opad 이중 해싱 구조

HMAC의 정의는 다음과 같습니다.

HMAC(K, M) = H((K' ⊕ opad) ∥ H((K' ⊕ ipad) ∥ M))

K' = H(K)       만약 |K| > 블록 크기 (SHA-256: 512비트)
K' = K ∥ 0...0  만약 |K| ≤ 블록 크기 (0으로 패딩)

ipad = 0x36 반복 (블록 크기만큼)
opad = 0x5C 반복 (블록 크기만큼)
HMAC — ipad/opad 이중 해싱 내부 해시 (Inner Hash) K' ⊕ ipad M H (SHA-256) 내부 해시값 외부 해시 (Outer Hash) K' ⊕ opad 내부 해시값 H (SHA-256) HMAC 태그 이중 해싱으로 길이 확장 공격 방어 — 내부 해시만으로는 HMAC 위조 불가

HMAC 보안 증명과 PRF 속성

HMAC의 보안은 기반 해시 함수의 압축 함수가 PRF(Pseudorandom Function)일 때 증명됩니다. Bellare(1996)의 증명에 따르면, 압축 함수가 PRF이면 HMAC도 PRF입니다. 이는 해시 함수 전체의 충돌 저항성이 깨져도 HMAC은 안전할 수 있음을 의미합니다 — 실제로 MD5의 충돌 저항성은 깨졌지만 HMAC-MD5는 여전히 안전한 것으로 간주됩니다(새 시스템에서는 권장하지 않습니다).

인증 암호화 (Authenticated Encryption)

인증 암호화(AEAD, Authenticated Encryption with Associated Data)는 기밀성과 무결성/인증을 동시에 제공합니다. 별도의 암호화+MAC 조합보다 안전하고 효율적이며, 현대 프로토콜(TLS 1.3, IPsec, WireGuard)의 표준입니다.

AES-GCM 심층 분석

GCM(Galois/Counter Mode)은 CTR 모드 암호화와 GHASH 인증을 결합한 AEAD 모드입니다. NIST SP 800-38D에 표준화되어 있으며, 하드웨어 가속(AES-NI + PCLMULQDQ)으로 매우 높은 처리량을 제공합니다.

GHASH — GF(2128) 곱셈

GHASH는 GF(2128) 위의 곱셈 기반 범용 해시 함수입니다. 기약 다항식은 x128 + x7 + x2 + x + 1이며, 해시 키 H = EK(0128)를 사용합니다.

/* GF(2^128) 곱셈: shift-and-XOR */
void gf128_mul(uint8_t *X, const uint8_t *Y)
{
    uint8_t Z[16] = {0};  /* 결과 */
    uint8_t V[16];
    memcpy(V, Y, 16);

    for (int i = 0; i < 128; i++) {
        /* X의 i번째 비트가 1이면 Z ^= V */
        if (X[i / 8] & (0x80 >> (i % 8)))
            xor_block(Z, V);

        /* V를 오른쪽 1비트 시프트 (MSB가 1이면 기약 다항식 XOR) */
        int msb = V[15] & 1;
        for (int j = 15; j > 0; j--)
            V[j] = (V[j] >> 1) | (V[j-1] << 7);
        V[0] >>= 1;
        if (msb)
            V[0] ^= 0xE1;  /* x^128 + x^7 + x^2 + x + 1의 상위 8비트 */
    }
    memcpy(X, Z, 16);
}

/* GHASH: 순차적 multiply-XOR 누적 */
/* GHASH(H, A, C):
 *   X_0 = 0^128
 *   X_i = (X_{i-1} XOR block_i) * H   (GF(2^128)에서)
 *   마지막: len(A)||len(C) 블록으로 마무리 */

태그 계산 전체 흐름

GCM 암호화+태그 생성의 전체 흐름입니다.

AES-GCM 암호화 + 태그 생성 흐름 H = E_K(0¹²⁸) J₀ = IV ∥ 0³¹ ∥ 1 (IV=96b) E_K(J₀+1) E_K(J₀+2) P₁ ⊕ P₂ ⊕ C₁ C₂ GHASH 인증 경로 AAD₁ ×H C₁ ×H len(A)∥len(C) ×H GHASH 결과 E_K(J₀) 인증 태그 (Tag)
Nonce 재사용 금지: AES-GCM에서 동일한 키로 Nonce를 재사용하면 GHASH 키 H가 복원되어 인증이 완전히 무력화됩니다. TLS 1.3은 레코드 시퀀스 번호를 Nonce로 사용하여 재사용을 구조적으로 방지합니다. Nonce 오용에 강한 대안으로 AES-GCM-SIV가 있습니다.

하드웨어 가속 매핑

AES-GCM의 두 핵심 연산은 각각 전용 하드웨어 명령어로 가속됩니다.

연산x86 명령어ARM 명령어용도
AES 블록 암호화AESENC, AESENCLASTAESE, AESMCGCTR 카운터 모드
GF(2128) 곱셈PCLMULQDQPMULL, PMULL2GHASH 계산
병렬 AES (AVX-512)VAES8블록 동시 처리
병렬 GF 곱셈VPCLMULQDQ8-way GHASH

상세한 하드웨어 가속 아키텍처는 Crypto API의 AES-NI 섹션을 참고하세요.

ChaCha20-Poly1305 심층 분석

ChaCha20-Poly1305는 ChaCha20 스트림 암호와 Poly1305 MAC을 결합한 AEAD 구성입니다. RFC 8439에 표준화되어 있으며, WireGuard와 TLS 1.3에서 AES-GCM의 대안으로 사용됩니다.

Poly1305 MAC — 소수체 연산

Poly1305는 소수 p = 2130 - 5 위의 다항식 평가(Polynomial Evaluation) MAC입니다. 메시지를 16바이트 블록으로 나누고, 각 블록을 정수로 해석하여 누적합니다.

/* Poly1305 MAC 핵심 연산 (간략화) */
/* r: 인증 키 (128비트, 클램핑됨), s: 일회용 키 (128비트) */
/* p = 2^130 - 5 */

accumulator = 0
for each 16-byte block m_i:
    n = le_bytes_to_num(m_i) + (1 << (8 * block_len))  /* 상위 비트 1 추가 */
    accumulator = (accumulator + n) * r  mod p

tag = (accumulator + s) mod 2^128  /* 하위 128비트 */

r 클램핑(Clamping): 효율적인 모듈러 리덕션을 위해 r의 특정 비트를 0으로 클리어합니다. 클램핑 마스크: 0x0ffffffc0ffffffc0ffffffc0fffffff. 이는 보안을 해치지 않으면서 곱셈 결과의 크기를 제한합니다.

Poly1305 누적 연산 — a = ((a + m_i) × r) mod p a = 0 + m₁∥0x01 × r mod p + m₂∥0x01 × r mod p + m_n∥0x01 × r + s Tag p = 2¹³⁰ − 5, r: 클램핑된 인증 키, s: 일회용 마스킹 키 각 m_i 블록은 16바이트 + 상위 비트 1 추가 (최대 17바이트 정수) 수학적 의미: 다항식 평가 tag = (m₁·r^n + m₂·r^(n-1) + … + m_n·r¹ + s) mod 2¹²⁸ 원타임 키(s)가 없으면 대수적 위조 공격 가능 → s는 반드시 일회용

AEAD 결합 프로토콜 (RFC 8439)

ChaCha20-Poly1305 AEAD 결합 프로토콜 1. Poly1305 키 생성 ChaCha20(Key, Nonce, counter=0) → r, s 2. ChaCha20 암호화 ChaCha20(Key, Nonce, counter=1) ⊕ Plaintext → C 3. Poly1305 인증 (r, s 사용) AAD (패딩) C (패딩) len(AAD)∥len(C) 8B+8B Poly1305(r, 위 데이터 연결) Tag (128비트) 출력: C ∥ Tag (암호문 + 인증 태그)

비대칭 암호 내부 구조 (Asymmetric Cryptography)

비대칭 암호(Public-Key Cryptography)는 공개키(Public Key)와 개인키(Private Key)의 쌍을 사용합니다. 키 교환, 디지털 서명, 암호화에 사용되며, 보안은 수학적 난제(소인수 분해, 이산 로그, 타원 곡선 이산 로그)에 기반합니다.

RSA 심층 분석

RSA 수학 — 모듈러 거듭제곱

RSA의 수학적 기반은 오일러 정리(Euler's Theorem)입니다.

모듈러 거듭제곱은 제곱-곱셈 알고리즘(Square-and-Multiply)으로 효율적으로 계산합니다.

/* 모듈러 거듭제곱: base^exp mod mod (MSB-first) */
uint64_t mod_exp(uint64_t base, uint64_t exp, uint64_t mod)
{
    uint64_t result = 1;
    base %= mod;

    for (int i = 63; i >= 0; i--) {  /* MSB부터 */
        result = (result * result) % mod;      /* 항상 제곱 */
        if (exp & (1ULL << i))
            result = (result * base) % mod;    /* 비트가 1이면 곱셈 */
    }
    return result;
}
/* 실제 구현은 다중 정밀도(multi-precision) 정수 라이브러리 필요 */

키 생성 — 소수 선택과 CRT 최적화

RSA 키 생성에서 소수 판정은 밀러-라빈(Miller-Rabin) 확률적 소수 판정법을 사용합니다. CRT(Chinese Remainder Theorem) 최적화는 복호화/서명 속도를 약 4배 향상시킵니다.

/* CRT 최적화 복호화 */
/* 사전 계산: dp = d mod (p-1), dq = d mod (q-1), qinv = q^(-1) mod p */
uint64_t rsa_decrypt_crt(uint64_t c, rsa_key *key)
{
    uint64_t m1 = mod_exp(c, key->dp, key->p);  /* c^dp mod p */
    uint64_t m2 = mod_exp(c, key->dq, key->q);  /* c^dq mod q */

    /* CRT 재결합 */
    int64_t h = (key->qinv * ((int64_t)m1 - m2)) % key->p;
    if (h < 0) h += key->p;

    return m2 + key->q * h;  /* m = m2 + q * h */
}
/* n 비트 거듭제곱 대신 n/2 비트 거듭제곱 2회 → ~4배 빠름 */

OAEP 패딩 (Optimal Asymmetric Encryption Padding)

원시 RSA(Textbook RSA)는 결정적(Deterministic)이고 변조 가능(Malleable)하므로 안전하지 않습니다. OAEP(PKCS#1 v2.2)는 무작위 시드를 사용하여 동일한 평문도 매번 다른 암호문을 생성합니다.

RSA-OAEP 인코딩 구조 seed (랜덤) DB = lHash ∥ PS(0x00...) ∥ 0x01 ∥ M MGF1(seed) maskedDB MGF1(maskedDB) seed maskedSeed EM = 0x00 ∥ maskedSeed ∥ maskedDB c = EM^e mod n (RSA 암호화)

RSA 공격 유형과 방어

공격대상방어
소인수 분해 (GNFS)작은 키 (≤1024비트)2048비트 이상 사용
BleichenbacherPKCS#1 v1.5 패딩OAEP 사용
타이밍 공격비 상수 시간 구현상수 시간 구현, 블라인딩
Coppersmith작은 e + 부분 키 노출적절한 키 크기

타원 곡선 암호 (Elliptic Curve Cryptography)

타원 곡선 암호(ECC)는 유한체 위의 타원 곡선에서 이산 로그 문제(ECDLP)의 어려움에 기반합니다. RSA보다 훨씬 작은 키로 동등한 보안을 제공합니다 (256비트 ECC ≈ 3072비트 RSA).

타원 곡선 수학 — 점 덧셈과 스칼라 곱셈

소수체(Prime Field) Fp 위의 단축 바이에르슈트라스(Short Weierstrass) 곡선: y² = x³ + ax + b (mod p)

점 덧셈 (P + Q = R): P = (x₁, y₁), Q = (x₂, y₂)일 때, 기울기 λ = (y₂ - y₁) / (x₂ - x₁) mod p, R = (λ² - x₁ - x₂, λ(x₁ - x_R) - y₁) mod p

점 더블링 (2P): λ = (3x₁² + a) / (2y₁) mod p

스칼라 곱셈 (kP): 더블-앤-애드(Double-and-Add) 알고리즘으로 O(log k)번의 점 연산으로 계산합니다. 이것이 공개키 = 개인키 × G의 계산 방법입니다.

/* 스칼라 곱셈: double-and-add (간략화) */
EC_POINT scalar_mul(const BIGNUM *k, const EC_POINT *P, const EC_GROUP *group)
{
    EC_POINT result = POINT_AT_INFINITY;  /* 항등원 */

    for (int i = BN_num_bits(k) - 1; i >= 0; i--) {
        result = point_double(result, group);  /* 항상 더블링 */
        if (BN_is_bit_set(k, i))
            result = point_add(result, P, group);  /* 비트=1이면 덧셈 */
    }
    return result;
}
/* 주의: 실제 구현은 사이드 채널 방어를 위해 Montgomery ladder 사용 */
타원 곡선 점 덧셈 — P + Q = R y² = x³ + ax + b (기하학적 해석) P Q -R R = P + Q x축 대칭 점 덧셈 공식 (P ≠ Q) P = (x₁, y₁), Q = (x₂, y₂) 기울기: λ = (y₂ − y₁) · (x₂ − x₁)⁻¹ mod p 결과 좌표: x_R = λ² − x₁ − x₂ mod p y_R = λ(x₁ − x_R) − y₁ mod p 점 더블링 공식 (P = Q → 2P) λ = (3x₁² + a) · (2y₁)⁻¹ mod p 이후 x_R, y_R 계산은 동일

점 덧셈의 기하학적 의미는 곡선 위의 두 점을 잇는 직선이 곡선과 만나는 세 번째 교점을 x축 대칭한 것입니다. 유한체에서는 실수 좌표 대신 모듈러 산술을 사용하지만 대수적 공식은 동일합니다. 나눗셈은 모듈러 역원으로 계산하며, 확장 유클리드 알고리즘이나 페르마 소정리(ap-2 mod p)를 사용합니다.

/* 타원 곡선 점 덧셈 (Fp, 아핀 좌표, 간략화) */
void ec_point_add(const EC_POINT *P, const EC_POINT *Q, EC_POINT *R,
                  const BIGNUM *a, const BIGNUM *p)
{
    BIGNUM lambda, t;

    if (ec_point_is_infinity(P)) { *R = *Q; return; }
    if (ec_point_is_infinity(Q)) { *R = *P; return; }

    if (BN_cmp(P->x, Q->x) == 0) {
        if (BN_cmp(P->y, Q->y) != 0) {
            ec_point_set_infinity(R);  /* P + (-P) = O */
            return;
        }
        /* 점 더블링: λ = (3x² + a) / (2y) mod p */
        BN_mod_sqr(&t, P->x, p);         /* x² */
        BN_mod_mul_word(&t, 3);           /* 3x² */
        BN_mod_add(&t, &t, a, p);        /* 3x² + a */
        BIGNUM dy; BN_mod_add(&dy, P->y, P->y, p);  /* 2y */
        BN_mod_inverse(&dy, &dy, p);     /* (2y)⁻¹ */
        BN_mod_mul(&lambda, &t, &dy, p); /* λ */
    } else {
        /* 점 덧셈: λ = (y₂ - y₁) / (x₂ - x₁) mod p */
        BIGNUM dy, dx;
        BN_mod_sub(&dy, Q->y, P->y, p);
        BN_mod_sub(&dx, Q->x, P->x, p);
        BN_mod_inverse(&dx, &dx, p);
        BN_mod_mul(&lambda, &dy, &dx, p);
    }
    /* x_R = λ² - x₁ - x₂, y_R = λ(x₁ - x_R) - y₁ */
    BN_mod_sqr(&R->x, &lambda, p);
    BN_mod_sub(&R->x, &R->x, P->x, p);
    BN_mod_sub(&R->x, &R->x, Q->x, p);
    BN_mod_sub(&t, P->x, &R->x, p);
    BN_mod_mul(&R->y, &lambda, &t, p);
    BN_mod_sub(&R->y, &R->y, P->y, p);
}

주요 곡선 매개변수

곡선체(Field) 크기보안 비트형태용도
P-256 (secp256r1)256비트128WeierstrassTLS, ECDSA, 가장 범용
P-384 (secp384r1)384비트192Weierstrass고보안 요구 (CNSA)
Curve25519255비트128MontgomeryX25519 키교환, Ed25519 서명

Curve25519는 Daniel Bernstein이 설계했으며, 상수 시간 구현이 용이하도록 Montgomery 래더(Ladder) 스칼라 곱셈을 사용합니다. 스칼라 클램핑으로 소그룹 공격을 구조적으로 방지합니다.

ECDSA 서명 생성/검증

서명 생성: 개인키 d, 메시지 해시 z = H(M)

  1. 무작위 k 선택 (1 ≤ k ≤ n-1), R = kG, r = R.x mod n
  2. s = k-1(z + r·d) mod n
  3. 서명 = (r, s)

서명 검증: 공개키 Q = dG

  1. u₁ = z·s-1 mod n, u₂ = r·s-1 mod n
  2. P = u₁·G + u₂·Q
  3. P.x mod n == r이면 유효
ECDSA 서명 생성 및 검증 프로토콜 서명 생성 (서명자) z = H(M) 메시지 해시 k ← 랜덤 (또는 RFC 6979) R = kG (스칼라 곱셈) r = R.x mod n s = k⁻¹ · (z + r·d) mod n 서명 (r, s) 서명 검증 (검증자) z = H(M), (r, s), Q u₁ = z·s⁻¹ mod n u₂ = r·s⁻¹ mod n P' = u₁·G + u₂·Q P'.x mod n == r ? 유효 : 거부 검증이 성립하는 이유: u₁G + u₂Q = (z/s)G + (r/s)dG = (z + rd)/s · G = kG = R
k 재사용 금지: ECDSA에서 동일한 k로 두 번 서명하면 개인키 d가 완전히 복원됩니다: d = (s₁·k - z₁) · r⁻¹ mod n. 2010년 PlayStation 3 해킹 사건이 이 취약점의 대표적 사례입니다. RFC 6979의 결정적 k 생성(HMAC-DRBG 기반)으로 방지합니다.

ECDH 키 교환

ECDH(Elliptic Curve Diffie-Hellman)는 타원 곡선 위에서 디피-헬먼 키 교환을 수행합니다.

ECDH 키 교환 프로토콜 Alice 개인키: a (비밀) 공개키: A = aG 공유 비밀: S = aB = abG Bob 개인키: b (비밀) 공개키: B = bG 공유 비밀: S = bA = baG A = aG (공개) B = bG (공개) aB = bA = abG (동일!) 도청자는 A, B, G만 알지만 a, b를 모르므로 abG 계산 불가 (ECDLP)

Diffie-Hellman 키 교환

고전 DH — 이산 로그 기반

고전 Diffie-Hellman은 소수 p와 생성자(Generator) g를 사용합니다. Alice는 a를 선택하여 A = ga mod p를 보내고, Bob은 b를 선택하여 B = gb mod p를 보냅니다. 공유 비밀은 S = gab mod p = Ab = Ba입니다.

Diffie-Hellman 키 교환 — 이산 로그 기반 공개 파라미터: 소수 p, 생성자 g Alice a ← 랜덤 비밀 A = g^a mod p S = B^a mod p = g^(ab) mod p Bob b ← 랜덤 비밀 B = g^b mod p S = A^b mod p = g^(ab) mod p A = g^a mod p B = g^b mod p 도청자: g, p, A=g^a, B=g^b를 알지만 a, b를 복원할 수 없음 (이산 로그 문제, DLP)

그룹 매개변수와 보안 강도

DH 그룹 크기보안 비트대응 ECC상태
1024비트80160비트비권장 (Logjam 공격)
2048비트112224비트최소 권장
3072비트128256비트권장
4096비트~140장기 보안
Logjam 공격: 2015년 공개된 Logjam 공격은 사전 계산(Precomputation)을 통해 512비트, 768비트 DH를 실시간으로 깰 수 있음을 보였습니다. 최소 2048비트를 사용하고, 가능하면 ECDH(X25519)로 대체하세요.

난수 생성 내부 구조 (Random Number Generation)

암호학적으로 안전한 난수(CSPRNG)는 키 생성, Nonce 생성, 초기화 벡터 생성의 기반입니다. 리눅스 커널의 난수 시스템은 하드웨어 엔트로피 소스에서 수집한 엔트로피를 DRBG로 확장하여 사용합니다.

DRBG (Deterministic Random Bit Generator)

DRBG는 NIST SP 800-90A에 표준화된 결정적 난수 생성기입니다. 시드(Seed)에서 결정적으로 출력을 생성하되, 시드의 엔트로피가 충분하면 출력이 진정한 난수와 구별 불가능합니다.

CTR-DRBG — AES 기반 결정적 난수

CTR-DRBG는 AES-CTR을 기반으로 합니다. 내부 상태는 키(K)와 값(V)의 쌍입니다.

/* CTR-DRBG 생성 (간략화) */
struct ctr_drbg_state {
    uint8_t key[32];    /* AES-256 키 */
    uint8_t V[16];      /* 128비트 카운터 */
    int reseed_counter;
};

void ctr_drbg_generate(struct ctr_drbg_state *s, uint8_t *out, size_t len)
{
    uint8_t temp[len];
    size_t offset = 0;

    while (offset < len) {
        increment_counter(s->V);  /* V++ (빅 엔디안) */
        aes_encrypt(s->key, s->V, temp + offset);  /* E_K(V) */
        offset += 16;
    }
    memcpy(out, temp, len);

    /* 상태 업데이트: 출력을 사용하여 K, V 갱신 */
    ctr_drbg_update(s, NULL);
    s->reseed_counter++;
}

/* Reseed: 새 엔트로피로 K, V 업데이트 */
/* 최대 2^48 요청 후 반드시 reseed 필요 */
CTR-DRBG 상태 머신 — Instantiate / Generate / Reseed Instantiate entropy + nonce + personal → seed → K, V 초기화 reseed_counter = 1 Generate 1. reseed_counter > 2⁴⁸ → Reseed 필요 2. 출력 블록 생성: V++ → E_K(V) → 출력 (16바이트씩 반복) 3. 상태 업데이트: K, V = Update(K, V, additional_data) reseed_counter++ 반복 Reseed 새 entropy 투입 → K, V 업데이트 reseed_counter = 1 의사 난수 출력

HMAC-DRBG — HMAC 기반 결정적 난수

HMAC-DRBG는 HMAC을 기반으로 하며, 블록 암호 없이 해시 함수만으로 구현 가능합니다. 리눅스 커널에서 ECDSA의 결정적 k 생성(RFC 6979)에 사용됩니다.

/* HMAC-DRBG 상태 업데이트 */
void hmac_drbg_update(uint8_t *K, uint8_t *V, const uint8_t *data, size_t len)
{
    /* K = HMAC(K, V || 0x00 || data) */
    K = hmac(K, V || 0x00 || data);
    V = hmac(K, V);

    if (data != NULL) {
        K = hmac(K, V || 0x01 || data);
        V = hmac(K, V);
    }
}

/* 생성: V = HMAC(K, V) 반복 → 출력 후 상태 업데이트 */

CTR-DRBG vs HMAC-DRBG 비교

속성CTR-DRBGHMAC-DRBG
기반 암호AESHMAC-SHA
상태 크기키 + 카운터K + V
최대 요청 간격248248
HW 가속AES-NI 활용SHA-NI 활용 가능
커널 기본기본 DRBGRFC 6979 (ECDSA)

Jitter RNG — CPU 타이밍 지터 기반 엔트로피

엔트로피 추정 원리

Jitter RNG는 CPU 실행 시간의 미세한 변동(Jitter)을 엔트로피 소스로 활용합니다. 캐시 미스, 분기 예측, TLB 플러시, 인터럽트 등으로 인해 동일한 연산도 매번 미세하게 다른 시간이 소요됩니다.

TSC(Time Stamp Counter)를 읽어 메모리 접근 연산의 실행 시간 차이(delta)를 수집하고, 반복 카운트 테스트(Repetition Count Test)와 적응적 비율 테스트(Adaptive Proportion Test)로 엔트로피 품질을 검증합니다.

컨디셔닝 함수

수집된 원시 샘플은 LFSR(Linear Feedback Shift Register) 기반 컨디셔닝으로 압축됩니다. 오버샘플링 비율(Oversampling Factor)은 보통 64:1 이상으로, 64개의 원시 샘플에서 1비트의 엔트로피를 추출합니다. 이는 보수적인 추정치로, 실제 엔트로피는 더 높을 수 있습니다.

Jitter RNG — CPU 타이밍 지터 수집 과정 TSC 읽기 (t₁) 메모리 접근 연산 (캐시 미스, 분기 예측 유발) TSC 읽기 (t₂) δ = t₂ − t₁ 건강 테스트 반복 카운트 + 적응적 비율 실패 시 δ 폐기 LFSR 컨디셔닝 64:1 압축 64회 반복 엔트로피 1비트 타이밍 변동 원인 (비결정적 요소) L1/L2/L3 캐시 미스 분기 예측 실패 TLB 플러시/미스 하드웨어 인터럽트 메모리 컨트롤러 경합 전력 상태 전환 (C-state)

Jitter RNG의 보안성은 Stephan Müller의 수학적 분석(SP 800-90B 기반)에 의해 검증되었으며, 리눅스 커널 5.0부터 CRNG의 부팅 초기 엔트로피 소스로 사용됩니다. RDRAND/RDSEED 하드웨어 RNG가 신뢰할 수 없는 환경(가상 머신, 에뮬레이터)에서도 엔트로피를 제공할 수 있는 소프트웨어 기반 대안입니다.

상세한 커널 RNG 아키텍처(ChaCha20 CRNG, 엔트로피 풀 등)는 Crypto API의 RNG 섹션hwrng 문서를 참고하세요.

키 유도 함수 (Key Derivation Functions)

키 유도 함수(KDF)는 비밀 재료(Secret Material)에서 하나 이상의 암호화 키를 유도합니다. 공유 비밀(ECDH 결과)이나 비밀번호처럼 직접 키로 사용하기에 부적합한 입력에서 고품질 키를 추출합니다.

HKDF (HMAC-based Key Derivation Function)

HKDF는 RFC 5869에 정의된 2단계 KDF입니다. 높은 엔트로피의 입력(ECDH 공유 비밀 등)에서 키를 유도할 때 사용됩니다.

Extract — 엔트로피 집중

PRK = HMAC-Hash(salt, IKM). 입력 키 재료(IKM)의 엔트로피를 고정 길이 PRK(Pseudorandom Key)로 집중합니다. salt는 선택적이지만, 도메인 분리와 엔트로피 추출의 견고성을 높여줍니다.

Expand — 키 재료 확장

PRK에서 원하는 길이의 OKM(Output Keying Material)을 생성합니다.

/* HKDF-Expand: PRK에서 OKM 확장 */
int hkdf_expand(const uint8_t *prk, size_t prk_len,
                const uint8_t *info, size_t info_len,
                uint8_t *okm, size_t okm_len)
{
    uint8_t T[HASH_LEN];
    size_t t_len = 0, offset = 0;
    int n = (okm_len + HASH_LEN - 1) / HASH_LEN;

    if (n > 255) return -EINVAL;  /* 최대 255 × HashLen */

    for (uint8_t i = 1; i <= n; i++) {
        /* T(i) = HMAC(PRK, T(i-1) || info || i) */
        hmac_init(prk, prk_len);
        if (t_len > 0) hmac_update(T, t_len);
        hmac_update(info, info_len);
        hmac_update(&i, 1);
        hmac_final(T);
        t_len = HASH_LEN;

        size_t copy = min(HASH_LEN, okm_len - offset);
        memcpy(okm + offset, T, copy);
        offset += copy;
    }
    return 0;
}
HKDF: Extract → Expand 파이프라인 Extract 단계 salt (선택적) IKM (키 재료) HMAC(salt, IKM) PRK Expand 단계 info (컨텍스트) T(1) = HMAC(PRK, info ∥ 0x01) T(2) = HMAC(PRK, T(1) ∥ info ∥ 0x02) T(n) OKM = T(1) ∥ T(2) ∥ … ∥ T(n) 의 처음 L 바이트 암호화 키 인증 키 IV / Nonce

커널 내 HKDF 사용 사례: fscrypt(마스터 키 → 파일별 키), kTLS(TLS 1.3 키 스케줄), WireGuard(Noise 프로토콜 체이닝 키). 구현은 fs/crypto/hkdf.c에 있습니다.

PBKDF2 (Password-Based Key Derivation Function 2)

PBKDF2(RFC 8018)는 비밀번호에서 암호화 키를 유도하는 함수입니다. 의도적으로 높은 계산 비용(반복)을 적용하여 무차별 대입을 지연시킵니다.

반복(Iteration)과 솔팅(Salting)

PBKDF2(Password, Salt, c, dkLen):
    for i = 1 to ceil(dkLen / hLen):
        U₁ = PRF(Password, Salt ∥ INT_32_BE(i))
        U₂ = PRF(Password, U₁)
        ...
        U_c = PRF(Password, U_{c-1})
        T_i = U₁ ⊕ U₂ ⊕ ... ⊕ U_c   (XOR 누적)

    DK = T₁ ∥ T₂ ∥ ... 의 처음 dkLen 바이트

솔트: 최소 128비트(16바이트) 권장. 레인보우 테이블 방어 및 동일 비밀번호의 도메인 분리. 반복 횟수: OWASP(2023)는 HMAC-SHA-256 기준 최소 600,000회를 권장합니다.

PBKDF2 반복 체인 — F 함수 (블록 i) Password Salt ∥ INT(i) HMAC U₁ HMAC U₂ HMAC U_c T_i = U₁ ⊕ U₂ ⊕ U₃ ⊕ … ⊕ U_c (XOR 누적, c = 반복 횟수) DK = T₁ ∥ T₂ ∥ … ∥ T_l c ≥ 600,000 (OWASP 권장)

PBKDF2 vs 현대 대안 비교

알고리즘메모리 사용GPU/ASIC 저항표준커널 지원
PBKDF2최소낮음RFC 8018지원
scrypt설정 가능 (MB~GB)높음RFC 7914미지원
Argon2id설정 가능 (MB~GB)매우 높음RFC 9106미지원 (cryptsetup 2.x)
커널 내 비밀번호 유도: LUKS2에서 Argon2는 사용자 공간(cryptsetup)에서 실행되며, 커널은 유도된 최종 키만 받아 dm-crypt에 사용합니다.

알고리즘 종합 비교

서로 다른 유형의 암호 알고리즘은 보안 강도, 성능 특성, 적용 환경이 상이합니다. 이 섹션에서는 대칭/비대칭/해시 알고리즘의 보안 수준 대응 관계, 실측 성능, 커널 서브시스템별 권장 알고리즘을 종합적으로 비교합니다.

보안 강도 수준 비교

암호학적 보안 강도는 "보안 비트(Security Bits)"로 표현됩니다. n비트 보안이란, 최선의 알려진 공격이 약 2n번의 연산을 요구한다는 의미입니다. 서로 다른 유형의 알고리즘이 동일한 보안 수준을 달성하기 위해 필요한 키 크기는 상이하며, 특히 ECC의 효율성이 두드러집니다. 128비트 보안에서 RSA는 3072비트가 필요하지만 ECC는 256비트면 충분합니다.

보안 강도별 키 크기 비교 (NIST SP 800-57)
보안 비트대칭 키RSA 키ECC 키해시 출력
1123TDEA2048비트224비트224비트
128AES-1283072비트256비트256비트
192AES-1927680비트384비트384비트
256AES-25615360비트512비트512비트

성능 특성 비교

주요 알고리즘 성능 (x86-64 참고치)
알고리즘SW 처리량HW 가속병렬화
AES-128-GCM~1 GB/s~10+ GB/s (AES-NI)높음
ChaCha20-Poly1305~1.5 GB/s~3 GB/s (AVX2)높음
SHA-256~500 MB/s~2+ GB/s (SHA-NI)낮음
RSA-2048 서명~1,000 op/s낮음
ECDSA P-256 서명~20,000 op/s낮음
X25519 키교환~30,000 op/s낮음
성능 참고사항: 실제 처리량은 CPU 아키텍처, 클럭 주파수, 데이터 크기, 구현 최적화에 따라 크게 달라집니다. AES-NI가 없는 ARM 환경에서는 ChaCha20-Poly1305가 AES-GCM보다 빠를 수 있습니다. 비대칭 연산(RSA, ECDSA)은 대칭 연산보다 수천 배 느리므로, TLS처럼 비대칭으로 키를 교환하고 대칭으로 데이터를 암호화하는 하이브리드 방식이 표준입니다.

커널 서브시스템별 알고리즘 선택 가이드

다음 표는 리눅스 커널의 주요 보안 서브시스템별 권장 알고리즘과 Crypto API 이름을 정리합니다. 알고리즘 선택 기준은 보안 강도, 하드웨어 가속 가용성, 프로토콜 표준 준수입니다.

사용 사례별 권장 알고리즘
사용 사례서브시스템권장 알고리즘Crypto API 이름
VPN 터널IPsecAES-256-GCMrfc4106(gcm(aes))
전송 암호화kTLSAES-GCM / ChaCha20-Poly1305gcm(aes)
디스크 암호화dm-cryptAES-256-XTSxts(aes)
파일 암호화fscryptAES-256-XTS + CTS-CBCxts(aes)
VPNWireGuardChaCha20-Poly1305rfc7539(chacha20,poly1305)
모듈 서명modsignRSA-4096+SHA-512 / ECDSApkcs1pad(rsa,sha512)
무결성IMASHA-256sha256
키 유도fscryptHKDF-SHA-512hmac(sha512)
난수 생성CRNGChaCha20 + Jitterstdrng

외부 참고 자료