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

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

전제 조건: 이 문서는 암호화 알고리즘의 내부 구조를 다룹니다. 리눅스 커널 Crypto API의 사용법(tfm, request, scatterlist 등)은 Linux Crypto Framework (Crypto API)에서 먼저 익히고, 하드웨어 가속은 암호화 하드웨어 가속, 드라이버 작성은 암호화 드라이버 구현 가이드를 참고하세요.
일상 비유: 이 문서는 자물쇠의 내부 기계 구조를 분해해 보는 수업과 비슷합니다. 열쇠 꽂는 방법(API 사용법)이 아니라, 자물쇠 내부의 핀·스프링·회전판(라운드 함수·치환 상자·모듈러 연산)이 어떻게 맞물려 "풀기 어려운 상태"를 만드는지를 들여다봅니다. 이 구조를 이해해야 어떤 알고리즘이 왜 안전한지, 왜 부채널(Side-Channel)에 취약해지는지를 판단할 수 있습니다.

핵심 요약

  • 대칭 암호(Symmetric Cipher) — 같은 키로 암·복호화(Decryption)합니다. AES, ChaCha20이 대표적이고 속도가 빠릅니다.
  • 공개키 암호(Public-key Cryptography) — 키 쌍(공개키·개인키)으로 기밀성·서명을 분리합니다. RSA, ECDSA, EdDSA가 대표적입니다.
  • 해시(Hash) 함수 — 입력을 고정 길이 요약으로 압축하는 단방향 함수. SHA-256, SHA-3 등이 있습니다.
  • 라운드(Round) 구조 — 치환·섞기 연산을 반복해 입력과 출력의 통계적 상관을 제거합니다. 라운드 수가 부족하면 즉시 깨집니다.
  • 수학적 기반 — 유한체(Galois Field), 모듈러 지수, 이산 로그, 타원 곡선(Elliptic Curve)이 "풀기 어려움"의 원천입니다.

단계별 이해

  1. 대칭 암호 내부 들여다보기
    AES의 S-box 치환, ShiftRows, MixColumns, AddRoundKey가 10~14 라운드 반복되며 확산과 혼동을 만드는 구조를 확인합니다.
  2. 해시 내부 구조
    SHA-256의 Merkle-Damgård 압축 함수와 SHA-3의 스펀지(Sponge) 구조가 왜 충돌 저항·원상 저항을 만드는지 살펴봅니다.
  3. 공개키: RSA
    큰 소수 두 개로 만든 모듈러 지수 연산(c = me mod n)이 정수 인수분해의 어려움에 기대어 안전성을 확보하는 원리를 이해합니다.
  4. 타원 곡선(Elliptic Curve)
    이산 로그 문제를 타원 곡선 위 점의 스칼라 곱으로 바꿔, 더 짧은 키로 같은 보안 강도를 제공하는 ECDSA/EdDSA 원리를 익힙니다.
  5. 구현 보안 고려
    부채널(Side-Channel)과 타이밍 공격을 막기 위한 상수 시간 구현, 메모리 지우기(memzero_explicit), 적절한 난수원이 왜 필수인지 정리합니다.

대칭 암호 (Symmetric Ciphers)

대칭 암호(Symmetric Cipher)는 암호화와 복호화(Decryption)에 동일한 키를 사용하는 암호 체계입니다. 송신자와 수신자가 같은 비밀 키를 공유해야 하므로 키 배포 문제가 있지만, 비대칭 암호보다 수천 배 빠르기 때문에 실제 데이터 암호화에 항상 대칭 암호가 사용됩니다. TLS, IPsec, WireGuard, dm-crypt 등 거의 모든 보안 프로토콜에서 핵심 데이터 암호화 계층은 대칭 암호입니다.

대칭 암호 vs 비대칭 암호 — 기본 구조 비교 대칭 암호 (Symmetric Cipher) 양쪽 모두 동일한 비밀 키 K 사용 송신자 (Alice) 평문 M E_K (암호화) 키 K 암호문 C 공개 채널 암호문 C D_K (복호화) 키 K 평문 M 수신자 (Bob) 빠름 ~10 GB/s 비대칭 암호 (Asymmetric / Public-Key Cipher) 암호화: 공개키 (PK) / 복호화: 개인키 (SK) — 서로 다른 키! 송신자 (Alice) 평문 M 암호화 Bob의 공개키 암호문 C 공개 채널 암호문 C 복호화 Bob의 개인키 ↑ 누구나 알 수 있음 ↑ Bob만 보유 (비밀) 평문 M 수신자 (Bob) 느림 ~1K op/s 실전: 하이브리드 암호화 (TLS, SSH, IPsec, WireGuard) 비대칭 암호로 대칭 키를 안전하게 교환 → 대칭 암호로 실제 데이터를 고속 암호화

대칭 암호는 크게 블록 암호(Block Cipher)와 스트림 암호(Stream Cipher)로 나뉩니다. 블록 암호는 고정 크기(AES: 128비트) 블록 단위로 데이터를 처리하며, 임의 길이 데이터를 다루려면 CBC, CTR 같은 운용 모드가 필요합니다. 스트림 암호는 키에서 연속적인 키스트림(Keystream)을 생성하여 평문과 XOR하므로 별도의 운용 모드 없이 임의 길이를 처리합니다. 리눅스 커널에서는 AES(블록 암호)와 ChaCha20(스트림 암호)이 가장 널리 사용됩니다.

좋은 암호 알고리즘은 두 가지 속성을 갖춰야 합니다. 혼돈(Confusion)은 키와 암호문의 관계를 복잡하게 만드는 것으로, AES에서는 S-Box(비선형 치환)가 이 역할을 합니다. 확산(Diffusion)은 평문의 작은 변화가 암호문 전체에 영향을 미치게 하는 것으로, AES에서는 ShiftRows와 MixColumns가 담당합니다. 이 두 속성의 조합으로 각 라운드를 반복하면 입력과 출력 사이의 통계적 관계가 완전히 파괴됩니다.

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는 다음과 같이 매핑(Mapping)됩니다.

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₁₅ 행 0 행 1 행 2 행 3 S[r][c] = b[r + 4c] 열 우선 (Column-Major) 예: S[1][2] = b[1+4×2] = b₉

상태 행렬의 각 바이트 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)로 미리 계산하여 사용합니다.

리눅스 커널의 crypto/aes_generic.c는 S-Box를 미리 계산된 4개의 Te 테이블(각 256×32비트)로 확장하여, SubBytes+ShiftRows+MixColumns를 단일 테이블 룩업 4회로 처리합니다. 이를 T-테이블(T-table) 최적화라 합니다.

/* crypto/aes_generic.c — T-테이블 기반 AES 라운드 최적화 */
/* Te0[x] = S[x].[02,01,01,03] — SubBytes+ShiftRows+MixColumns 통합 */
static const u32 Te0[256] = {
    0xc66363a5U, 0xf87c7c84U, 0xee777799U, 0xf67b7b8dU,
    0xfff2f20dU, 0xd66b6bbdU, 0xde6f6fb1U, 0x91c5c554U,
    /* ... 나머지 248개 엔트리 ... */
};

/* AES-128 1라운드: 4번의 테이블 룩업으로 처리 */
/* s0..s3: 상태 행렬의 4개 열 (각 32비트 워드) */
t0 = Te0[s0 >> 24] ^ Te1[(s1 >> 16) & 0xff] ^
     Te2[(s2 >> 8) & 0xff] ^ Te3[s3 & 0xff] ^ rk[4];
t1 = Te0[s1 >> 24] ^ Te1[(s2 >> 16) & 0xff] ^
     Te2[(s3 >> 8) & 0xff] ^ Te3[s0 & 0xff] ^ rk[5];
t2 = Te0[s2 >> 24] ^ Te1[(s3 >> 16) & 0xff] ^
     Te2[(s0 >> 8) & 0xff] ^ Te3[s1 & 0xff] ^ rk[6];
t3 = Te0[s3 >> 24] ^ Te1[(s0 >> 16) & 0xff] ^
     Te2[(s1 >> 8) & 0xff] ^ Te3[s2 & 0xff] ^ rk[7];

Te0~Te3 각 테이블은 S-Box 출력과 MixColumns 행렬의 서로 다른 행을 사전 결합한 것입니다. Te0[x]는 S(x)에 대해 {02, 01, 01, 03} 열을 곱한 32비트 결과이고, Te1~Te3은 바이트 순환 시프트 변형입니다. 이 방식은 라운드당 16번의 테이블 룩업과 16번의 XOR만으로 SubBytes+ShiftRows+MixColumns+AddRoundKey 전체를 완료합니다.

T-테이블 트레이드오프: T-테이블 최적화는 4KB(4×256×4바이트)의 메모리를 소비하지만 성능이 우수합니다. 그러나 테이블 접근 패턴이 캐시(Cache) 타이밍 공격에 노출될 수 있어, 보안 민감 환경에서는 AES-NI 하드웨어 명령어를 우선 사용합니다. 커널은 aes_generic을 폴백(Fallback)으로만 등록하고, aes-aesni(x86) 또는 aes-ce(ARM)가 우선 선택됩니다.
SubBytes — 각 바이트를 S-Box로 독립 치환 0x53 입력 바이트 행: 5 열: 3 S-Box [256 바이트] GF(2⁸) 역원 + 아핀 변환 0xED 출력 바이트 S-Box 부분 테이블 행\열 2 3 4 4 98 09 83 5 00 ED 2C 6 B7 FD 93 예: S-Box[0x53] → 행 5, 열 3 → 0xED 모든 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₃₂ 시프트 0 시프트 1 시프트 2 시프트 3 열 간 바이트 확산 — SubBytes의 변화가 여러 열로 전파

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

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

MixColumns — MDS 행렬 곱셈 수식 (GF(2⁸)) s'₀ s'₁ s'₂ s'₃ = 02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 × s₀ s₁ s₂ s₃ GF(2⁸) 위의 행렬 곱셈 굵은 숫자(02, 03)가 확산 역할 순환 MDS 행렬 → 최대 확산 보장 4열 독립 처리 → 병렬화 가능

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 (항등원) 예: xtime(0x57) 0x57≪1 = 0xAE, MSB=0 → 0xAE ⊕ 0x00 = 0xAE ∴ {02}·{57} = 0xAE

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 암호화/복호화의 진입점(Entry Point)은 crypto/aes_generic.ccrypto_aes_encrypt()/crypto_aes_decrypt()입니다. 다음은 커널의 실제 암호화 함수 구조입니다.

/* crypto/aes_generic.c — 커널 AES 암호화 함수 (간략화) */
void crypto_aes_encrypt(struct crypto_tfm *tfm,
                        u8 *out, const u8 *in)
{
    const struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);
    const u32 *rk = ctx->key_enc;  /* 확장 키 (44/52/60 워드) */
    u32 s0, s1, s2, s3, t0, t1, t2, t3;
    int nrounds = ctx->key_length / 4 + 6;  /* 10/12/14 */

    /* 초기 AddRoundKey (K_0) */
    s0 = get_unaligned_be32(in     ) ^ rk[0];
    s1 = get_unaligned_be32(in +  4) ^ rk[1];
    s2 = get_unaligned_be32(in +  8) ^ rk[2];
    s3 = get_unaligned_be32(in + 12) ^ rk[3];
    rk += 4;

    /* 중간 라운드: T-테이블 룩업으로 통합 처리 */
    for (int r = 1; r < nrounds; r++) {
        t0 = Te0[s0 >> 24] ^ Te1[(s1 >> 16) & 0xff] ^
             Te2[(s2 >> 8) & 0xff] ^ Te3[s3 & 0xff] ^ rk[0];
        /* ... t1, t2, t3도 동일 패턴 ... */
        s0 = t0; s1 = t1; s2 = t2; s3 = t3;
        rk += 4;
    }

    /* 마지막 라운드: MixColumns 없이 S-Box만 사용 */
    s0 = (Te2[(t0 >> 24)] & 0xff000000) ^
         (Te3[(t1 >> 16) & 0xff] & 0x00ff0000) ^
         (Te0[(t2 >> 8) & 0xff] & 0x0000ff00) ^
         (Te1[ t3 & 0xff] & 0x000000ff) ^ rk[0];
    /* ... s1, s2, s3 동일 ... */

    put_unaligned_be32(s0, out     );
    put_unaligned_be32(s1, out +  4);
    put_unaligned_be32(s2, out +  8);
    put_unaligned_be32(s3, out + 12);
}

마지막 라운드는 MixColumns가 없으므로, Te 테이블에서 S-Box 바이트만 추출(마스킹)하여 결과를 구성합니다. ctx->key_enccrypto_aes_expand_key()로 사전에 확장된 라운드 키 배열이며, ctx->key_dec에는 InvMixColumns가 적용된 복호화용 키가 별도로 저장됩니다.

보안 속성과 알려진 공격

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

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

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

AES 같은 블록 암호는 한 번에 딱 128비트(16바이트)만 처리할 수 있습니다. 그러나 실제 암호화할 데이터(파일, 네트워크 패킷(Packet) 등)는 대부분 16바이트보다 훨씬 큽니다. 이 문제를 해결하는 것이 운용 모드(Mode of Operation)입니다. 운용 모드는 여러 블록을 어떤 순서와 방식으로 처리할지 정의하며, 모드 선택에 따라 보안성, 성능, 에러 복구 특성이 크게 달라집니다.

운용 모드를 선택할 때 고려해야 할 핵심 속성은 다음과 같습니다.

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

ECB(Electronic Codebook)는 가장 단순한 운용 모드로, 각 블록을 독립적으로 암호화합니다: Ci = EK(Pi). 하지만 이 단순함이 치명적인 약점입니다 — 동일한 평문 블록은 항상 동일한 암호문을 생성하므로, 원본 데이터의 패턴이 암호문에 그대로 보존됩니다.

ECB 모드의 패턴 보존 문제 — 동일 블록 = 동일 암호문 평문 블록들 A B A A C ⚠ ECB 암호문 — 패턴 노출! X Y X X Z A→X, A→X, A→X: 같은 블록 = 같은 결과 같은 평문(A, B, A, A, C) CBC 암호문 — 패턴 완전 은닉 M N P Q R 동일 평문(A)도 위치에 따라 완전히 다른 암호문 이전 블록의 암호문이 다음 블록 암호화에 영향 ECB는 단일 블록 암호화(키 래핑 등) 외에는 사용 금지

CBC (Cipher Block Chaining) 모드

CBC(Cipher Block Chaining)는 ECB의 패턴 보존 문제를 해결하는 가장 직관적인 방식입니다. 핵심 아이디어는 이전 블록의 암호문을 다음 블록의 암호화에 섞는 것입니다. 이렇게 하면 동일한 평문 블록이라도 이전 블록이 다르면 완전히 다른 암호문이 됩니다. 첫 블록에는 이전 암호문이 없으므로, 랜덤한 초기화 벡터(IV, Initialization Vector)를 대신 사용합니다. IV는 비밀일 필요는 없지만, 반드시 매번 새로운 값이어야 합니다.

암호화는 블록 간 의존성으로 직렬화(Serialization)되지만, 복호화는 병렬 수행이 가능합니다. 블록 크기의 배수가 아닌 데이터는 패딩(Padding)(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(Counter) 모드는 블록 암호를 스트림 암호처럼 사용하는 모드입니다. 평문을 직접 암호화하는 대신, Nonce(Number used Once, 한 번만 사용하는 고유 번호)와 순차적으로 증가하는 카운터(Counter)를 결합한 값을 AES로 암호화하여 키스트림을 만들고, 이를 평문과 XOR합니다. CBC와 달리 각 블록의 키스트림 생성이 완전히 독립적이므로 병렬 처리가 가능하며, 패딩도 불필요합니다.

모든 블록이 독립적이므로 완전한 병렬 처리가 가능하며, 패딩이 불필요합니다. 그러나 동일한 키-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) 모드

디스크 암호화에는 CBC나 CTR과 다른 특수한 요구사항이 있습니다. 디스크는 섹터 단위(보통 512B~4KB)로 임의 접근해야 하므로, 전체 체인을 순회하는 CBC는 부적합합니다. 또한 암호문 크기가 평문과 정확히 같아야 하므로(디스크 섹터 크기 고정), 인증 태그를 붙이는 GCM도 쓸 수 없습니다. XTS(XEX-based Tweaked CodeBook)는 이러한 디스크 암호화의 제약을 해결하기 위해 설계된 IEEE P1619 표준 모드입니다.

XTS의 핵심 아이디어는 트윅(Tweak)입니다. 트윅은 섹터 번호와 블록 인덱스에서 유도되는 값으로, 같은 평문이라도 디스크의 다른 위치에서는 다른 암호문을 생성합니다. 두 개의 키(K1: 데이터 암호화, K2: 트윅 암호화)를 사용합니다.

트윅은 동일한 평문이 다른 위치에서 다른 암호문을 생성하게 하며, 디스크의 각 섹터가 독립적으로 접근 가능합니다. 리눅스 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¹²⁸) dm-crypt / fscrypt

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전체 무효화(Invalidation)OTLS, IPsec, kTLS

리눅스 커널의 블록 암호 운용 모드는 crypto/cbc.c, crypto/ctr.c, crypto/xts.c 등에 구현됩니다. Crypto API의 skcipher 인터페이스를 통해 접근하며, scatterlist 기반 비연속 메모리를 직접 처리합니다.

/* crypto/cbc.c — CBC 모드 암호화 커널 구현 (간략화) */
static int crypto_cbc_encrypt(struct skcipher_request *req)
{
    struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
    struct skcipher_walk walk;
    int err;

    err = skcipher_walk_virt(&walk, req, false);

    while (walk.nbytes) {
        u8 *src = walk.src.virt.addr;
        u8 *dst = walk.dst.virt.addr;
        u8 *iv  = walk.iv;             /* 이전 암호문 블록 (또는 IV) */
        int nbytes = walk.nbytes;

        do {
            crypto_xor(iv, src, bsize);  /* P_i ⊕ C_{i-1} (XOR) */
            crypto_cipher_encrypt_one(cipher, iv, iv); /* E_K() */
            memcpy(dst, iv, bsize);      /* C_i = E_K(P_i ⊕ C_{i-1}) */
            src += bsize;
            dst += bsize;
        } while ((nbytes -= bsize) >= bsize);

        err = skcipher_walk_done(&walk, nbytes);
    }
    return err;
}
/* crypto/ctr.c — CTR 모드 커널 구현 (간략화) */
/* CTR 모드는 카운터를 암호화하여 키스트림 생성 → 평문과 XOR */
static int crypto_ctr_crypt(struct skcipher_request *req)
{
    struct skcipher_walk walk;
    u8 keystream[16];  /* AES 블록 크기 */
    int err;

    err = skcipher_walk_virt(&walk, req, false);

    while (walk.nbytes >= bsize) {
        /* E_K(CTR) → 키스트림 블록 */
        crypto_cipher_encrypt_one(cipher, keystream, walk.iv);
        /* 키스트림 ⊕ 평문 → 암호문 */
        crypto_xor_cpy(walk.dst.virt.addr,
                       walk.src.virt.addr, keystream, bsize);
        /* 카운터 증가 (빅 엔디안) */
        crypto_inc(walk.iv, bsize);  /* CTR++ */
        err = skcipher_walk_done(&walk, walk.nbytes - bsize);
    }
    /* 마지막 부분 블록 처리 (패딩 불필요, 스트림 특성) */
    return err;
}

skcipher_walk은 scatterlist를 순회하며 가상 주소(Virtual Address) 매핑을 관리하는 커널 헬퍼입니다. CBC는 블록 체인 구조상 암호화가 순차적이지만, CTR은 카운터만 증가하므로 이론적으로 병렬화가 가능합니다. 실제로 AES-NI 구현(arch/x86/crypto/aesni-intel_glue.c)은 CTR 모드에서 여러 블록을 파이프라이닝하여 처리합니다.

ChaCha20 스트림 암호 내부 구조

AES가 S-Box(룩업 테이블)와 유한체 곱셈을 사용하는 반면, ChaCha20은 오직 세 가지 기본 산술 연산만으로 동작합니다: ARX = Add(32비트 모듈러 덧셈) + Rotate(비트 회전) + XOR(배타적 논리합). 이 세 연산은 모든 CPU에서 네이티브로 지원되고, 데이터 의존적 분기(Branch)나 메모리 접근이 없으므로 본질적으로 상수 시간(Constant-Time) 실행이 보장됩니다. 즉, AES와 달리 캐시 타이밍 사이드 채널 공격에 면역입니다.

ChaCha20은 Daniel Bernstein이 설계한 스트림 암호(Stream Cipher)입니다. AES-NI 하드웨어 가속이 없는 플랫폼(일부 ARM, MIPS)에서도 AES보다 빠르며, WireGuard VPN과 TLS 1.3의 기본 암호로 채택되었습니다. 리눅스 커널의 CRNG(암호학적 난수 생성기) 내부에서도 ChaCha20이 핵심 스트림 암호로 사용됩니다.

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

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

ChaCha20 상태 행렬 (512비트 = 16 × 32비트 워드) "expa" "nd 3" "2-by" "te k" ← 상수 (0x61707865, ...) 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비트) 행 0 행 1 행 2 행 3 "expand 32-byte k" — Nothing-up-my-sleeve 상수 (의도적 약점 배제 증명)

"expand 32-byte k" ASCII 상수(리틀 엔디안(Endianness))는 아무 의미 없는(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 없이 산술 연산만 사용 → 상수 시간 실행, 타이밍 공격 면역 ⊲ n = 좌측 비트 회전(Left Rotate) n비트. 예: d ⊲ 16 → d를 왼쪽으로 16비트 회전 더블 라운드(Double Round) = 컬럼 라운드 + 대각선 라운드 컬럼 라운드 — 같은 열의 4워드 QR(0, 4, 8, 12) QR(1, 5, 9, 13) QR(2, 6, 10, 14) QR(3, 7, 11, 15) 다음 대각선 라운드 — 대각선의 4워드 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 구현은 lib/crypto/chacha.c에 범용(Generic) 버전이, arch/x86/crypto/chacha_glue.c에 SIMD 최적화 버전이 있습니다. 커널 CRNG(암호학적 난수 생성기)에서도 ChaCha20을 핵심 스트림 암호로 사용합니다.

/* lib/crypto/chacha.c — 커널 ChaCha20 범용 구현 */
void chacha_crypt_generic(u32 *state, u8 *dst,
                         const u8 *src, unsigned int bytes,
                         int nrounds)
{
    u32 buf[CHACHA_BLOCK_SIZE / sizeof(u32)]; /* 64바이트 키스트림 */

    while (bytes >= CHACHA_BLOCK_SIZE) {
        chacha_block_generic(state, buf, nrounds);
        crypto_xor_cpy(dst, src, (u8 *)buf, CHACHA_BLOCK_SIZE);
        bytes -= CHACHA_BLOCK_SIZE;
        src += CHACHA_BLOCK_SIZE;
        dst += CHACHA_BLOCK_SIZE;
        state[12]++;  /* 카운터 증가 (state[12] = counter) */
    }
    if (bytes) {
        /* 마지막 부분 블록: 키스트림 생성 후 필요한 만큼만 XOR */
        chacha_block_generic(state, buf, nrounds);
        crypto_xor_cpy(dst, src, (u8 *)buf, bytes);
    }
}

/* drivers/char/random.c — 커널 CRNG도 ChaCha20 사용 */
/* crng_fast_key_erasure(): ChaCha20으로 난수 생성 후 */
/* 키를 즉시 폐기하여 forward secrecy 보장 */

chacha_block_generic()은 20라운드(ChaCha20) 또는 12라운드(ChaCha12, XChaCha)를 수행합니다. x86에서는 chacha_4block_avx2()가 AVX2 SIMD로 4블록을 동시에 처리하여 처리량(Throughput)을 극대화합니다. 커널 CRNG(drivers/char/random.c)는 ChaCha20으로 난수를 생성한 직후 키를 덮어씀(Fast Key Erasure)으로써, 상태 노출 시에도 이전 출력을 복원할 수 없도록 전방 비밀성(Forward Secrecy)을 보장합니다.

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) 함수 (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₀) 256비트 압축 함수 f M₀ (512b) 압축 함수 f M₁ (512b) 압축 함수 f M_{n-1} (512b) 해시값 H(M) — 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] 16 → 64 워드 확장 σ₀, σ₁: 소문자 시그마 — 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) ← h, 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 구현은 lib/crypto/sha256.c에 있으며, 다음과 같이 압축 함수를 처리합니다.

/* lib/crypto/sha256.c — 커널 SHA-256 압축 함수 (간략화) */
static void sha256_transform(u32 *state, const u8 *input)
{
    u32 a, b, c, d, e, f, g, h;
    u32 W[64], t1, t2;
    int i;

    /* W[0..15]: 입력 블록에서 빅 엔디안으로 추출 */
    for (i = 0; i < 16; i++)
        W[i] = get_unaligned_be32(input + 4 * i);

    /* W[16..63]: σ₀, σ₁ 함수로 확장 */
    for (i = 16; i < 64; i++)
        W[i] = sigma1(W[i - 2]) + W[i - 7] +
               sigma0(W[i - 15]) + W[i - 16];

    /* 작업 변수 초기화: 현재 해시 상태에서 복사 */
    a = state[0]; b = state[1]; c = state[2]; d = state[3];
    e = state[4]; f = state[5]; g = state[6]; h = state[7];

    /* 64라운드 압축 */
    for (i = 0; i < 64; i++) {
        t1 = h + Sigma1(e) + Ch(e, f, g) + SHA256_K[i] + W[i];
        t2 = Sigma0(a) + Maj(a, b, c);
        h = g; g = f; f = e;
        e = d + t1;
        d = c; c = b; b = a;
        a = t1 + t2;
    }

    /* 해시 상태에 누적 덧셈 */
    state[0] += a; state[1] += b; state[2] += c; state[3] += d;
    state[4] += e; state[5] += f; state[6] += g; state[7] += h;
}

x86 시스템에서 SHA-NI 명령어(Intel Goldmont+, AMD Zen+)가 지원되면 arch/x86/crypto/sha256_ni_asm.S의 어셈블리(Assembly) 구현이 우선 사용되어 처리량이 소프트웨어 대비 3~5배 향상됩니다. crypto_shash_update()를 호출하면 커널이 자동으로 최적 구현을 선택합니다.

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

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과 동일한 Merkle-Damgård 구조와 압축 함수 설계를 사용하지만, 모든 연산이 64비트 워드 기반으로 확장됩니다. 64비트 CPU(x86-64, ARM64)에서는 네이티브 64비트 연산을 사용하므로 SHA-256보다 오히려 더 빠른 경우가 많습니다. 반면 32비트 CPU에서는 64비트 연산을 에뮬레이션해야 하므로 상당히 느려집니다. 이런 이유로 리눅스 커널의 모듈 서명은 SHA-512를 기본으로 사용하지만, IMA 등 임베디드 환경도 고려해야 하는 서브시스템은 SHA-256을 사용합니다.

속성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 64바이트 (SHA-256) M H (SHA-256) 내부 해시값 외부 해시 (Outer Hash) K' ⊕ opad 64바이트 (SHA-256) 내부 해시값 H (SHA-256) HMAC 태그 256비트 이중 해싱으로 길이 확장 공격 방어 — 내부 해시만으로는 HMAC 위조 불가

HMAC 보안 증명과 PRF 속성

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

리눅스 커널의 HMAC 구현(crypto/hmac.c)은 ipad/opad 해시 상태를 키 설정 시점에 사전 계산하여 캐싱합니다. 이를 통해 매 HMAC 연산마다 키 패딩을 반복할 필요 없이, 캐싱된 중간 해시 상태에서 즉시 시작할 수 있습니다.

/* crypto/hmac.c — 커널 HMAC 구현 (사전 계산 최적화) */
struct hmac_ctx {
    struct crypto_shash *hash;    /* 기반 해시 (SHA-256 등) */
    u8 ipad_state[];              /* H(K' ⊕ ipad) 중간 상태 */
    u8 opad_state[];              /* H(K' ⊕ opad) 중간 상태 */
};

/* setkey: ipad/opad 해시 상태를 사전 계산 */
static int hmac_setkey(struct crypto_shash *parent,
                      const u8 *inkey, unsigned int keylen)
{
    /* 키가 블록 크기보다 길면 해시 */
    if (keylen > bs)
        crypto_shash_digest(shash, inkey, keylen, ipad);

    /* ipad = K' ⊕ 0x36..., opad = K' ⊕ 0x5C... */
    for (i = 0; i < bs; i++) {
        ipad[i] ^= 0x36;
        opad[i] ^= 0x5C;
    }

    /* 중간 해시 상태 캐싱: init → update(pad) → export */
    crypto_shash_init(shash);
    crypto_shash_update(shash, ipad, bs);
    crypto_shash_export(shash, ipad_state);  /* 상태 저장 */

    crypto_shash_init(shash);
    crypto_shash_update(shash, opad, bs);
    crypto_shash_export(shash, opad_state);  /* 상태 저장 */
    return 0;
}

/* finup: 캐싱된 상태에서 시작하여 HMAC 계산 */
static int hmac_finup(struct shash_desc *pdesc,
                      const u8 *data, unsigned int nbytes,
                      u8 *out)
{
    /* 내부 해시 완료: H(ipad_state || msg) → 다이제스트 */
    crypto_shash_finup(desc, data, nbytes, out);

    /* 외부 해시: opad 상태 복원 → update(내부 해시) → final */
    crypto_shash_import(desc, opad_state);
    return crypto_shash_finup(desc, out, ds, out);
}

이 사전 계산 최적화 덕분에, 동일한 키로 여러 메시지의 HMAC을 계산할 때 키 패딩+해싱 단계를 반복하지 않아 성능이 크게 향상됩니다. IPsec처럼 패킷마다 HMAC을 계산하는 고빈도 환경에서 특히 중요합니다.

인증 암호화 (Authenticated Encryption)

AES-CBC나 AES-CTR 같은 암호화 모드는 데이터를 읽을 수 없게 만들지만(기밀성), 데이터가 변조되었는지는 감지하지 못합니다. 공격자가 암호문의 비트를 뒤집으면, 복호화 결과도 예측 가능하게 변조됩니다(특히 CTR 모드에서 1비트 변경은 평문의 정확히 1비트를 변경합니다). 따라서 암호화와 별도로 MAC(Message Authentication Code)을 붙여 무결성(Integrity)을 검증해야 하는데, 이 조합을 잘못하면 심각한 보안 취약점(Vulnerability)이 발생합니다.

암호화만 (Encryption Only) vs 인증 암호화 (AEAD) 암호화만 — 변조 감지 불가 평문 M AES-CTR 암호문 C 공격자: C의 비트 변조 C' = C ⊕ Δ → 평문도 M' = M ⊕ Δ 수신자: 변조 감지 못함! AEAD — 변조 즉시 감지 평문 M AES-GCM (암호화+인증) 암호문 C 태그 T 공격자: C' 변조 시도 태그 T 재계산 불가 (키 필요) 수신자: T 불일치 → 거부!

인증 암호화(AEAD, Authenticated Encryption with Associated Data)는 기밀성과 무결성/인증을 하나의 알고리즘으로 동시에 제공합니다. 암호화 결과에 인증 태그(Tag)가 함께 생성되며, 복호화 시 태그가 일치하지 않으면 데이터를 거부합니다. AEAD의 "AD(Associated Data)"는 암호화하지는 않지만 인증은 해야 하는 데이터(예: 패킷 헤더, 프로토콜 버전)를 의미합니다. TLS 1.3, IPsec, WireGuard 등 현대 프로토콜은 모두 AEAD만 허용합니다.

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¹²⁸) GCTR 암호화 경로 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₀) 초기 카운터 암호화 GHASH 결과 인증 태그 (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

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

커널에서 AES-GCM은 crypto/gcm.c에 구현되어 있으며, AEAD 인터페이스(struct aead_request)를 통해 암호화+인증을 한 번의 호출로 처리합니다. IPsec은 RFC 4106 형식을, kTLS는 표준 GCM 형식을 사용합니다.

/* crypto/gcm.c — 커널 GCM AEAD 암호화 (흐름 간략화) */
static int crypto_gcm_encrypt(struct aead_request *req)
{
    struct crypto_gcm_req_priv_ctx *pctx = ...;

    /* 1단계: GHASH(AAD) — 인증 데이터 해싱 */
    gcm_hash_assoc(req, pctx);

    /* 2단계: CTR 모드 암호화 (J₀+1부터 시작) */
    crypto_skcipher_encrypt(skreq);  /* AES-CTR */

    /* 3단계: GHASH(C) — 암호문 해싱 */
    gcm_hash_ciphertext(req, pctx);

    /* 4단계: GHASH(len(A)||len(C)) + E_K(J₀) → 태그 */
    gcm_hash_len_done(req, pctx);

    /* 태그를 암호문 뒤에 붙임 */
    crypto_gcm_encrypt_done(req);
    return 0;
}

/* 복호화 시: 태그 검증 실패하면 -EBADMSG 반환 */
/* → 평문을 0으로 덮어쓰고 호출자에게 오류 전달 */
복호화 태그 검증: GCM 복호화에서 태그 검증이 실패하면 커널은 복호화된 평문을 0으로 덮어쓴 후 -EBADMSG를 반환합니다. 이는 인증되지 않은 데이터가 상위 계층에 전달되는 것을 원천 차단하는 보안 조치입니다. 애플리케이션은 반드시 반환값을 확인해야 합니다.

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 mod p + 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 결합 프로토콜 Poly1305 키 생성 ChaCha20(Key, Nonce, counter=0) → r, s ChaCha20 암호화 ChaCha20(Key, Nonce, counter=1) ⊕ Plaintext → C counter=0 → Poly1305 키 전용 / counter≥1 → 암호화 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) — 를 사용하여 이 문제를 해결합니다.

공개키는 누구에게나 공개하고, 개인키는 소유자만 비밀로 보관합니다. 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있고(암호화), 개인키로 서명한 데이터는 공개키로 검증할 수 있습니다(서명). 비대칭 암호의 보안은 수학적 난제에 기반합니다 — 공개키에서 개인키를 역으로 계산하는 것이 현실적으로 불가능한 수학 문제(소인수 분해, 이산 로그, 타원 곡선 이산 로그)를 활용합니다.

비대칭 암호의 핵심 — 일방향 함수(Trapdoor One-Way Function) 정방향: 매우 쉬움 (밀리초) RSA: 곱셈 쉬움 (p × q = n) ECC: 스칼라 곱 쉬움 (Q = dG) 다항 시간 O(n²) / O(n log n) 역방향: 사실상 불가능 RSA: 소인수 분해 어려움 (n → p, q) ECC: 이산 로그 어려움 (Q → d) 준지수 시간 / 수십억 년 트랩도어: 비밀키로 쉬움 RSA: p, q를 알면 d 계산 가능 ECC: d를 알면 복호화/서명 가능 개인키 = 트랩도어 (지름길) 비대칭 암호의 세 가지 핵심 용도 키 교환 (ECDH, DH) 대칭 키를 안전하게 공유 디지털 서명 (RSA, ECDSA, EdDSA) 인증 + 무결성 + 부인 방지 암호화 (RSA-OAEP) 소량 데이터만 (키, 세션 토큰)
하이브리드 암호화: 비대칭 암호는 대칭 암호보다 수천 배 느리므로(RSA-2048: ~1,000 op/s vs AES-GCM: ~10 GB/s), 실제 대용량 데이터를 직접 암호화하는 데는 사용하지 않습니다. 대신 비대칭 암호로 대칭 키를 안전하게 교환한 후, 그 대칭 키로 데이터를 암호화하는 하이브리드 방식이 표준입니다. TLS, SSH, PGP 등 모든 현대 프로토콜이 이 방식을 사용합니다.

RSA 심층 분석

RSA 수학 — 모듈러 거듭제곱

RSA는 가장 오래되고 널리 사용되는 비대칭 암호 알고리즘입니다. 1977년 Rivest, Shamir, Adleman이 발표했으며, 큰 수의 소인수 분해가 어렵다는 사실에 기반합니다. 두 큰 소수 p, q를 곱하면 n = p×q를 쉽게 구할 수 있지만, n만 주어졌을 때 p와 q를 찾는 것은 현재 알려진 알고리즘으로 천문학적인 시간이 필요합니다. 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 + 부분 키 노출적절한 키 크기

커널의 RSA 구현은 crypto/rsa.c에 있으며, MPI(Multi-Precision Integer) 라이브러리(lib/mpi/)를 사용합니다. 모듈 서명 검증(Signature Verification)이 주요 사용처이며, CRT 최적화와 타이밍 공격 방어를 위한 블라인딩(Blinding)을 지원합니다.

/* crypto/rsa.c — 커널 RSA 서명 검증 (간략화) */
static int rsa_verify(struct akcipher_request *req)
{
    struct rsa_mpi_key *pkey = &ctx->key;
    MPI s, m;

    /* 서명값 s를 MPI로 변환 */
    s = mpi_read_raw_from_sgl(req->src, req->src_len);

    /* m = s^e mod n (공개키 연산) */
    m = mpi_alloc(0);
    mpi_powm(m, s, pkey->e, pkey->n);

    /* 결과를 출력 scatterlist에 기록 */
    mpi_write_to_sgl(m, req->dst, req->dst_len, ...);

    mpi_free(s);
    mpi_free(m);
    return 0;
}

/* lib/mpi/mpi-pow.c — 모듈러 거듭제곱 (제곱-곱셈) */
/* mpi_powm(): Montgomery 리덕션 + 슬라이딩 윈도우 최적화 */
/* 2048비트 RSA 검증: ~1,000 op/s (소프트웨어) */

mpi_powm()은 Montgomery 곱셈과 슬라이딩 윈도우 기법으로 최적화된 모듈러 거듭제곱입니다. 커널은 RSA 개인키 연산(서명, 복호화)보다 공개키 연산(검증)을 훨씬 자주 수행합니다 — 부팅 시 수백 개의 모듈 서명을 검증하지만, 서명 생성은 사용자 공간(User Space)에서 처리합니다.

타원 곡선 암호 (Elliptic Curve Cryptography)

타원 곡선 암호(ECC, Elliptic Curve Cryptography)는 RSA에 비해 훨씬 작은 키로 동등한 보안을 제공하는 비대칭 암호 방식입니다. 256비트 ECC 키는 3,072비트 RSA 키와 동등한 128비트 보안을 제공합니다. 키 크기가 작으면 연산 속도, 메모리 사용량, 네트워크 전송량 모두에서 유리하므로, TLS 1.3에서는 ECDHE(타원 곡선 디피-헬먼)가 키 교환의 기본값입니다.

타원 곡선 암호의 보안은 타원 곡선 이산 로그 문제(ECDLP)에 기반합니다. 곡선 위의 기저점(Base Point) G에 정수 d를 곱하여 Q = dG를 계산하는 것은 쉽지만(스칼라 곱셈), Q와 G만 주어졌을 때 d를 역으로 찾는 것은 현재 알려진 알고리즘으로 사실상 불가능합니다. 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 계산은 동일 ※ 유한체(Fp)에서는 시각화 불가 — 대수 공식은 동일

점 덧셈의 기하학적 의미는 곡선 위의 두 점을 잇는 직선이 곡선과 만나는 세 번째 교점을 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) 메시지 해시 1 k ← 랜덤 (또는 RFC 6979) R = kG (스칼라 곱셈) 2 r = R.x mod n s = k⁻¹ · (z + r·d) mod n 3 d = 비밀(개인키) 서명 (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)

EdDSA / Ed25519 — 에드워즈 곡선 서명

EdDSA(Edwards-curve Digital Signature Algorithm)는 RFC 8032에 표준화된 타원 곡선 디지털 서명 방식입니다. ECDSA의 단점(랜덤 k 의존성, 복잡한 구현)을 해결하기 위해 Daniel Bernstein이 설계했으며, 결정적(Deterministic) 서명 생성, 빠른 검증, 간결한 구현이 특징입니다. 리눅스 커널에서는 모듈 서명과 IMA 서명의 대안으로 지원됩니다.

Curve25519와 에드워즈 형식

Ed25519는 비꼬인 에드워즈 곡선(Twisted Edwards Curve) -x² + y² = 1 + dx²y² (mod p)를 사용합니다. p = 2255 - 19이며, 기저점(Base Point) B의 위수(Order)는 소수 ℓ입니다. 에드워즈 형식의 점 덧셈 공식은 통합 공식(Unified Formula)으로, P = Q인 경우와 아닌 경우를 분기 없이 동일하게 처리하여 사이드 채널 안전성을 높입니다.

속성ECDSA (P-256)Ed25519
곡선 형식WeierstrassTwisted Edwards
서명 크기64바이트64바이트
키 크기32바이트32바이트
k 생성랜덤 (또는 RFC 6979)결정적 (해시 기반)
상수 시간 구현어려움 (Weierstrass 특수 케이스)용이 (통합 공식)
서명 속도~20,000 op/s~60,000 op/s
검증 속도~8,000 op/s~15,000 op/s

서명 생성과 검증

Ed25519 서명의 핵심 차이점은 난수를 사용하지 않는다는 것입니다. 대신 개인키의 해시에서 nonce를 결정적으로 유도합니다.

Ed25519 서명 생성:
  (a, prefix) = SHA-512(private_key)    -- 64바이트 해시의 전반부=스칼라, 후반부=prefix
  A = aB                                 -- 공개키 (압축된 점)
  r = SHA-512(prefix || M) mod ℓ         -- 결정적 nonce (메시지 의존)
  R = rB                                 -- nonce 점
  S = (r + SHA-512(R || A || M) · a) mod ℓ
  서명 = (R, S)  -- 총 64바이트

Ed25519 서명 검증:
  k = SHA-512(R || A || M) mod ℓ
  SB == R + kA ?                         -- 그룹 방정식 검증
Ed25519 서명 생성 — 결정적 nonce 서명 생성 SHA-512(개인키) a (스칼라) prefix r = SHA-512(prefix ∥ M) mod ℓ 랜덤 불필요! R = rB (스칼라 곱셈) S = (r + SHA-512(R ∥ A ∥ M) · a) mod ℓ 서명 (R, S) 64바이트 서명 검증 입력: (R, S), A, M k = SHA-512(R ∥ A ∥ M) mod ℓ 좌변: SB 우변: R + kA SB == R + kA ? 유효 SB = (r + ka)B = rB + kaB = R + kA ✓

Ed25519의 결정적 nonce 생성은 ECDSA의 가장 위험한 취약점(k 재사용/편향)을 원천적으로 제거합니다. 동일 메시지에 대해 항상 동일한 서명이 생성되므로, 테스트와 재현성도 우수합니다.

커널 Ed25519 구현

리눅스 커널의 Ed25519 구현은 crypto/ecdsasignature.asn1crypto/asymmetric_keys/에 있으며, 주로 모듈 서명 검증에 사용됩니다. 서명 검증만 커널에서 수행하고, 서명 생성은 사용자 공간의 sign-file 유틸리티가 담당합니다.

/* crypto/ecrdsa.c 및 asymmetric_keys — 커널 EdDSA 검증 흐름 */
/* 1. 공개키 파싱: ASN.1 DER에서 A (32바이트) 추출 */
/* 2. 서명 파싱: (R, S) 각 32바이트 */
/* 3. 검증 방정식: SB == R + SHA-512(R || A || M) · A */

/* 커널 모듈 서명 검증 사용 예 */
/* kernel/module/signing.c */
static int mod_verify_sig(const void *mod, struct load_info *info)
{
    struct public_key_signature *pks;

    pks = mod_make_digest(info->hdr, info->len);
    return verify_pkcs7_signature(
        mod, modlen,
        info->sig, info->sig_len,
        VERIFY_USE_SECONDARY_KEYRING,
        VERIFYING_MODULE_SIGNATURE,
        NULL, NULL
    );
    /* 내부적으로 RSA-PKCS#1 또는 ECDSA/EdDSA 검증 수행 */
}
Ed25519 vs Ed448: Ed448은 Curve448 기반의 224비트 보안 EdDSA 변형입니다. Ed25519가 128비트 보안이므로 대부분의 환경에서 충분하지만, 장기 보안이 필요한 경우 Ed448을 고려할 수 있습니다. 리눅스 커널은 현재 Ed25519를 주로 지원합니다.

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) ⚠ 중간자 공격(MITM)에 취약 — 별도 인증 필요 (서명, 인증서)

그룹 매개변수와 보안 강도

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

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

암호화의 모든 보안은 궁극적으로 예측 불가능한 난수에 의존합니다. AES 키, RSA 소수, ECDSA의 서명 nonce, TLS의 세션 ID — 이 모든 값이 예측 가능하면 아무리 강력한 알고리즘이라도 무력화됩니다. 암호학에서 "좋은 난수"란 단순히 통계적으로 균일한 것이 아니라, 과거 출력을 모두 관찰해도 다음 출력을 예측할 수 없는 난수입니다. 이를 CSPRNG(Cryptographically Secure Pseudo-Random Number Generator)이라 합니다.

난수 생성의 핵심 개념은 엔트로피(Entropy)입니다. 엔트로피는 "예측 불가능성의 양"을 비트 단위로 측정한 것입니다. 예를 들어 공정한 동전 던지기 1회는 1비트의 엔트로피를, 256면 주사위 1회는 8비트의 엔트로피를 제공합니다. 리눅스 커널은 하드웨어 인터럽트(Interrupt) 타이밍, CPU 타이밍 지터(Jitter), RDRAND 명령어 등 다양한 소스에서 엔트로피를 수집하고, 이를 DRBG(결정적 난수 생성기)에 공급하여 필요한 만큼의 난수를 생성합니다.

리눅스 커널 난수 생성 파이프라인 — 엔트로피 → DRBG → 출력 엔트로피 소스 인터럽트 타이밍 (IRQ) CPU 지터 (Jitter RNG) RDRAND/RDSEED (x86) 디바이스 노이즈 예측 불가능한 물리적 현상 부팅 초기: 엔트로피 부족 주의 수집 엔트로피 풀 컨디셔닝 → 시드 256비트 품질 시딩 DRBG ChaCha20 기반 결정적 확장 256비트 시드 → 무한 출력 암호학적 난수 출력 getrandom() 시스콜 /dev/urandom get_random_bytes() (커널) AES 키, IV, Nonce, 서명 k 예측 불가능 + 역추적 불가능

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 의사 난수 출력 (요청당 최대 2¹⁹ 비트)

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)을 엔트로피 소스로 활용합니다. 캐시 미스, 분기 예측(Branch Prediction), TLB 플러시(Flush), 인터럽트 등으로 인해 동일한 연산도 매번 미세하게 다른 시간이 소요됩니다.

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)

암호화 알고리즘은 고품질의 균일 분포(Uniform Distribution) 키를 요구하지만, 실제로 얻어지는 비밀 재료는 대부분 이 조건을 충족하지 못합니다. 예를 들어 ECDH 키 교환의 공유 비밀은 타원 곡선 위의 x좌표로, 비트가 균일하지 않습니다. 사용자 비밀번호는 엔트로피가 매우 낮고(보통 20~40비트), 직접 키로 사용하면 무차별 대입에 취약합니다.

키 유도 함수(KDF, Key Derivation Function)는 이런 부적합한 입력에서 고품질의 암호화 키를 추출하는 함수입니다. KDF는 크게 두 가지로 나뉩니다. HKDF처럼 이미 높은 엔트로피를 가진 입력(ECDH 공유 비밀, 랜덤 시드)에서 여러 키를 유도하는 유형과, PBKDF2/Argon2처럼 낮은 엔트로피 입력(비밀번호)에 의도적 연산 비용을 부과하여 무차별 대입을 지연(Latency)시키는 유형입니다.

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 2023 권장)

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에 사용합니다.

포스트 양자 암호화 (Post-Quantum Cryptography)

현재 사용되는 RSA, ECDSA, ECDH 등 비대칭 암호 알고리즘은 모두 소인수 분해이산 로그 문제의 어려움에 기반합니다. 그런데 충분히 강력한 오류 정정 양자 컴퓨터가 실현되면, Shor 알고리즘이 이 문제들을 다항 시간에 풀 수 있습니다. 따라서 RSA와 타원 곡선 기반 공개키는 장기적으로 구조적인 양자 취약성을 가집니다.

이에 대비하여 NIST는 2024년 8월 13일에 양자 컴퓨터로도 풀기 어려운 새로운 수학 문제에 기반한 포스트 양자 암호화(PQC) 표준 FIPS 203, FIPS 204, FIPS 205를 최종 발표했습니다. 한편, 대칭 암호(AES)와 해시 함수(SHA-256)는 양자 위협이 상대적으로 작습니다. Grover 알고리즘이 보안 강도를 줄이지만, AES-256을 사용하면 장기 보안 마진을 더 넉넉하게 가져갈 수 있습니다.

실무 연결: 이 섹션은 수학적 배경 설명에 집중합니다. TLS, WireGuard, IPSec, Secure Boot, 모듈 서명, TPM, 공급망 같은 실제 전환 경계는 포스트 양자 보안 전환 문서에서 실무 중심으로 따로 정리합니다.

양자 컴퓨팅 기초 — 왜 기존 암호가 위험한가

포스트 양자 암호를 이해하려면 양자 컴퓨터가 기존 암호를 깰 수 있는지 먼저 알아야 합니다. 핵심은 양자 비트(큐비트, Qubit)의 특성에 있습니다.

큐비트(Qubit)와 양자 중첩

고전 컴퓨터의 비트는 0 또는 1 중 하나의 값만 가집니다. 반면 양자 컴퓨터의 큐비트(Qubit)중첩(Superposition) 상태에 놓일 수 있어, 0과 1을 동시에 표현합니다. 비유하자면, 고전 비트가 "앞면" 또는 "뒷면"이 확정된 동전이라면, 큐비트는 공중에서 회전하는 동전처럼 앞면과 뒷면의 가능성을 동시에 품고 있습니다.

고전 비트 vs 큐비트 — 양자 병렬성의 기원 고전 비트 (Classical Bit) 0 확정 상태 또는 1 확정 상태 큐비트 (Qubit) |ψ⟩ |0⟩ |1⟩ α|0⟩ + β|1⟩ 0과 1을 동시에 표현 측정 전: |α|²+|β|²=1 측정 시: 0 또는 1 확정 양자 병렬성 — n 큐비트가 2ⁿ 상태를 동시에 처리 고전 3비트 → 한 번에 1개 상태만 처리 010 8가지 경우(000~111)를 하나씩 순서대로 확인 양자 3큐비트 → 8개 상태를 동시에 처리 (중첩) |000⟩+|001⟩+|010⟩+|011⟩ +|100⟩+|101⟩+|110⟩+|111⟩ n=300이면 2³⁰⁰ ≈ 우주 원자 수

핵심은 양자 병렬성(Quantum Parallelism)입니다. n개의 큐비트가 중첩 상태에 있으면, 하나의 연산으로 2n개의 입력을 동시에 처리할 수 있습니다. 다만 측정 시에는 하나의 결과만 얻을 수 있으므로, 양자 알고리즘의 핵심은 "원하는 답의 확률을 증폭시키는" 기법(양자 간섭)에 있습니다.

Shor 알고리즘 — RSA가 깨지는 원리

RSA의 보안은 "두 큰 소수의 곱 N = p × q에서 p와 q를 찾는 것이 어렵다"는 가정에 기반합니다. 고전 컴퓨터의 최선의 알고리즘(GNFS)도 하위지수(sub-exponential) 시간이 필요합니다. 그런데 Shor 알고리즘은 이를 다항 시간 O(n³)으로 단축합니다.

Shor 알고리즘 — 소인수 분해의 양자 지름길 문제: N = p × q 에서 p, q를 찾아라 예: N = 15 → p = 3, q = 5 고전 컴퓨터 (무차별 시도) 2로 나누어볼까? N mod 2 = 1 → 실패 3으로 나누어볼까? N mod 3 = 0 → 성공! ... RSA-2048: 약 √N ≈ 2¹⁰²⁴개의 후보를 시도해야 최선 (GNFS): exp(1.9 × nⁿ⁵⁳ × ln(n)²⁵⁳) RSA-2048 해독 시간 현존 최고 슈퍼컴퓨터로 수십억 년 → 안전 양자 컴퓨터 (Shor 알고리즘) ① 랜덤 a 선택, f(x) = aˣ mod N 정의 핵심 통찰: f(x)는 주기 r을 가진 주기함수 ② 양자 푸리에 변환(QFT)으로 주기 r 탐색 2ⁿ개 상태를 동시에 처리 → 간섭으로 r 증폭 ③ 주기 r 발견 → gcd(aʳ² ± 1, N) = p 또는 q 시간 복잡도: O(n³) — n은 비트 수 RSA-2048: 오류 정정 양자 컴퓨터가 생기면 급격히 취약
일상 비유 — 자물쇠 공장: RSA는 "두 큰 소수를 곱하기는 쉽지만, 결과에서 원래 소수를 찾기는 어렵다"는 원리입니다. 마치 두 색 물감을 섞기는 쉽지만 섞인 물감에서 원래 색을 분리하기는 어려운 것과 같습니다. 그런데 양자 컴퓨터의 Shor 알고리즘은 "섞인 물감의 주기적 패턴"을 읽어서 원래 색을 역추적(Backtrace)하는 특수 장비에 해당합니다 — 고전 컴퓨터에는 이 장비가 없습니다.

같은 원리가 ECC(타원 곡선)의 이산 로그 문제와 DH 키 교환에도 적용됩니다. Shor 알고리즘의 변형이 이산 로그 문제도 다항 시간에 풀 수 있으므로, RSA뿐 아니라 현재 사용되는 모든 비대칭 암호가 양자 컴퓨터에 취약합니다.

Grover 알고리즘 — 대칭 암호에 미치는 영향

Grover 알고리즘은 정렬되지 않은 데이터베이스에서 원하는 항목을 √N번 만에 찾는 양자 알고리즘입니다. 암호학에서 이것은 n비트 키를 가진 대칭 암호의 보안 강도를 n/2비트로 줄이는 것을 의미합니다. AES-128의 경우 64비트 보안으로 약화되어 위험하지만, AES-256은 128비트 보안이 유지되므로 양자 시대에도 충분합니다.

양자 알고리즘이 암호 체계에 미치는 영향 요약
양자 알고리즘공격 대상영향대응 전략
ShorRSA, ECDSA, ECDH, DH완전 파괴 — 다항 시간 해독PQC 알고리즘으로 교체 필수
GroverAES, ChaCha20, SHA보안 강도 절반 — 128비트→64비트키 길이 2배 (AES-256 사용)

PQC 알고리즘 패밀리 분류

양자 컴퓨터에 저항하는 암호 알고리즘은 기반으로 삼는 수학 문제에 따라 크게 네 가지 패밀리로 분류됩니다. 각 패밀리는 서로 다른 보안 가정에 의존하므로, 한 패밀리에서 취약점이 발견되더라도 다른 패밀리는 영향받지 않습니다.

포스트 양자 암호 — 네 가지 수학적 패밀리 ① 격자 기반 (Lattice-Based) 기반 문제: LWE (Learning With Errors), Module-LWE, NTRU NIST 표준: ML-KEM (FIPS 203), ML-DSA (FIPS 204) 장점: 빠른 연산 속도, 합리적 키 크기, KEM+서명 모두 지원 단점: 비교적 새로운 수학적 가정 (수십 년 vs 수백 년) ★ PQC 주력 — 대부분의 프로토콜에 권장 ② 해시 기반 (Hash-Based) 기반 문제: 해시 함수의 충돌/프리이미지 저항성 NIST 표준: SLH-DSA (FIPS 205), XMSS (RFC 8391) 장점: 가장 보수적 보안 가정, 수십 년간 검증된 해시 함수 단점: 큰 서명 크기 (수~수십 KB), 서명만 지원 (KEM 불가) ★ 보수적 대안 — 격자 암호 대비 "보험" ③ 코드 기반 (Code-Based) 기반 문제: 오류 정정 코드 복호화 (Syndrome Decoding) 대표 알고리즘: Classic McEliece, HQC (NIST 4라운드) 장점: 40년 이상 연구된 오래된 보안 가정 (1978년 McEliece) 단점: 매우 큰 공개키 (수십~수백 KB), 통신에 부적합 ★ ML-KEM 대안 후보 — 격자와 독립된 보안 다양성 확보 ④ 다변수 기반 (Multivariate) 기반 문제: 다변수 이차 방정식 시스템 (MQ 문제) 대표 알고리즘: Rainbow (파기됨), GeMSS, UOV 장점: 매우 빠른 서명 검증, 짧은 서명 가능 단점: 큰 공개키, Rainbow(NIST 3라운드 후보)가 공격당해 파기됨 ★ 연구 단계 — NIST 표준 선정 실패, 신뢰도 하락
NIST 표준화 결과: NIST는 2024년 최종 표준으로 격자 기반 2개(ML-KEM, ML-DSA)와 해시 기반 1개(SLH-DSA)를 선정했습니다. 추가로 격자 기반 FN-DSA(Falcon, FIPS 206 초안)와 코드 기반 HQC가 후속 표준화를 진행 중입니다. 다변수 기반 Rainbow는 2022년 Beullens의 공격으로 파기되었으며, 아이소지니(Isogeny) 기반 SIKE도 2023년 고전 공격에 의해 파기되어 현재 NIST PQC 표준에서 제외되었습니다.

격자 암호의 직관적 이해

NIST PQC 표준의 핵심인 격자(Lattice) 기반 암호를 이해하기 위해 일상적인 비유부터 시작합니다.

일상 비유 — 시끄러운 교실: 선생님(비밀키 보유자)이 학생에게 정답을 알려주되, 의도적으로 약간의 오차(소음)를 섞어 전달합니다. 교실 안의 선생님은 원래 정답을 알기에 소음을 걸러낼 수 있지만, 교실 밖에서 도청하는 사람(공격자)은 정답과 소음이 뒤섞인 결과만 들을 수 있습니다. 수천 명이 동시에 말하는 운동장에서 특정 한 사람의 목소리를 분리하는 것이 사실상 불가능한 것처럼, 높은 차원의 격자에서 소음이 추가된 결과로부터 원래 비밀을 복원하는 것은 양자 컴퓨터로도 효율적으로 풀 수 없습니다.
격자 암호 직관 — 왜 양자 컴퓨터로도 어려운가 2차원 격자 — 최근접 벡터 문제 (CVP) 정답 (비밀 s) b = As+e (오차로 이탈) e (오차) 2D에서는 눈으로 가장 가까운 격자점을 찾을 수 있지만 수백 차원에서는 모든 방향이 "가까워 보여서" 양자 컴퓨터로도 효율적으로 풀 수 없습니다 차원이 높아지면 왜 어려운가? 2차원: 격자점 주변에 빈 공간이 넓음 → 가장 가까운 점을 직관적으로 식별 가능 수십 차원: 격자점 밀도 급증 → 수많은 후보점이 비슷한 거리에 밀집 수백 차원 (ML-KEM: 256차원): → 격자점 중 정답 식별 불가 (차원의 저주) 최선의 공격도 지수 시간 (양자/고전 동일) → Shor 무력

이것이 격자 기반 암호가 양자 안전한 핵심 이유입니다. Shor 알고리즘은 주기적 구조(정수의 곱셈 구조, 타원 곡선의 이산 로그)를 이용하지만, 격자 문제에는 Shor가 활용할 수 있는 주기적 구조가 존재하지 않습니다. 현재까지 알려진 최선의 격자 공격 알고리즘은 양자 컴퓨터를 사용하더라도 고전 컴퓨터와 본질적으로 같은 시간 복잡도를 보입니다.

"Harvest Now, Decrypt Later" 위협 모델

양자 컴퓨터가 아직 실현되지 않았더라도 PQC 전환이 급한 이유가 있습니다. "Harvest Now, Decrypt Later"(HNDL) 공격 모델에서 적대자는 현재 암호화된 네트워크 트래픽을 대량으로 수집하여 저장한 뒤, 미래에 충분히 강력한 양자 컴퓨터가 등장하면 이를 복호화합니다. 정부 기밀, 의료 기록, 금융 데이터처럼 10~30년 이상 기밀성이 유지되어야 하는 데이터는 지금 이 순간에도 양자 위협에 노출되어 있습니다.

Harvest Now, Decrypt Later — 양자 위협의 시간축 시간 1 현재 — 트래픽 수집 적대자가 RSA/ECDH 암호화된 TLS, VPN, SSH 트래픽을 대량으로 캡처 · 저장 2 대기 — 암호문 보관 암호화된 데이터를 수년~수십 년 장기 보관 (저장 비용 매우 저렴) 현재 기술로는 복호화 불가 3 미래 — 양자 복호화 암호학적으로 유효한 양자 컴퓨터로 Shor 알고리즘 실행 → RSA/ECDH 키 복원 → 평문 획득 Shor 알고리즘 — 비대칭 암호 완전 파괴 RSA (소인수 분해): 2048비트 → 다항 시간에 해독 ECDH/ECDSA (이산 로그): P-256 → 다항 시간에 해독 DH (이산 로그): 2048비트 → 다항 시간에 해독 Grover 알고리즘 — 대칭 암호 보안 반감 AES-128: 128비트 → 64비트 보안 (위험) AES-256: 256비트 → 128비트 보안 (안전) SHA-256: 충돌 저항 유지 (안전)
시간이 곧 보안입니다: 미국 국가 안보 시스템은 2035년 전후를 목표로 양자 취약 알고리즘을 단계적으로 퇴출하는 방향이 분명해졌습니다. 기밀 데이터의 유효 기간이 그 시점을 넘으면, 지금부터 PQC 전환 계획을 시작해야 합니다.

NIST PQC 표준 알고리즘

NIST FIPS 203/204/205 (2024년 확정)
표준알고리즘유형기반 문제키 크기용도
FIPS 203ML-KEM (CRYSTALS-Kyber)KEMModule-LWE공개키 800~1568B키 교환/캡슐화(Encapsulation)
FIPS 204ML-DSA (CRYSTALS-Dilithium)서명Module-LWE/SIS공개키 1312~2592B디지털 서명
FIPS 205SLH-DSA (SPHINCS+)서명해시 기반공개키 32~64B보수적 대안 서명
NIST PQC 3대 표준 — 각각 무엇을 보호하는가 ML-KEM (FIPS 203) ① 키 캡슐화 (KEM) 역할: 두 당사자가 안전하게 공유 비밀을 설정하는 메커니즘 사용처: • TLS 1.3 핸드셰이크 • SSH 키 교환 • VPN (IPsec IKEv2, WireGuard) 일상 비유: 상대방의 열린 자물쇠(공개키)에 비밀 메모를 넣고 잠그면, 열쇠 주인(비밀키 보유자)만 열어볼 수 있습니다. 기반: Module-LWE (격자) ML-DSA (FIPS 204) ② 디지털 서명 역할: 메시지의 출처와 무결성을 수학적으로 증명하는 서명 사용처: • TLS 인증서 서명 • 커널 모듈 서명 검증 • 소프트웨어 패키지 서명 일상 비유: 공증인의 도장과 같습니다. 누구나 도장의 진위를 확인할 수 있지만, 도장을 찍을 수 있는 것은 본인만 가능합니다. 기반: Module-LWE/SIS (격자) SLH-DSA (FIPS 205) ③ 보수적 서명 (해시 기반) 역할: 격자 암호에 대한 대안 해시 함수만으로 서명 생성/검증 사용처: • 장기 보관 문서 서명 • 펌웨어/부트로더 서명 • 루트 CA 인증서 일상 비유: 금고 안에 보관하는 비상 열쇠 입니다. 평소에는 ML-DSA를 사용하되, 격자 암호가 깨질 경우 이것으로 대체합니다. 기반: 해시 함수 (SHA/SHAKE)

알고리즘 선택 가이드 — 언제 무엇을 사용하는가

세 가지 표준은 서로 다른 역할을 담당하므로 경쟁 관계가 아닌 상호 보완 관계입니다. 다음 표는 사용 시나리오별 권장 알고리즘을 정리합니다.

시나리오별 PQC 알고리즘 선택 가이드
시나리오필요 기능권장 알고리즘이유
TLS 1.3 키 교환KEMML-KEM-768 (하이브리드)빠른 속도, 합리적 크기, 브라우저 지원
커널 모듈(Kernel Module) 서명서명ML-DSA-65빠른 검증, 부팅 시 다수 모듈 검증에 적합
루트 CA 인증서서명SLH-DSA-128s 또는 ML-DSA-87장기 보관, 보수적 보안 (CA는 수십 년 유효)
펌웨어(Firmware) 서명서명SLH-DSA-128f 또는 LMS해시 기반 안전성, 격자 가정 불필요
SSH 키 교환KEM + 서명ML-KEM-768 + ML-DSA-65키 교환과 호스트 인증 모두 양자 내성
IPsec VPNKEMML-KEM-768 (IKEv2)RFC 9370 기반 PQ 키 교환
코드 서명 (최대 보안)서명ML-DSA-87 + SLH-DSA-128s이중 서명으로 보안 다양성 확보

격자 기반 암호의 핵심 — LWE 문제

격자(Lattice) 기반 암호의 보안은 LWE(Learning With Errors) 문제에 기반합니다. 공개 행렬 A와 비밀 벡터 s에 대해, b = As + e (mod q)에서 작은 오차(Error) 벡터 e가 포함된 결과로부터 s를 복원하는 것이 어렵다는 가정입니다.

LWE (Learning With Errors) — 격자 암호의 기반 문제 행렬 A n × m (공개) mod q × s (비밀) m × 1 작은 계수 + e (오차) n × 1 가우시안 분포 = b (공개) n × 1 mod q LWE 문제의 어려움 A, b가 주어졌을 때 s를 복원하는 것은 오차 e 때문에 최근접 벡터 문제(CVP)와 동치이며, 격자 차원이 크면 양자 컴퓨터로도 효율적으로 풀 수 없다고 믿어집니다 → 최선: 2^(n/log n) 시간 (양자/고전 동일) 양자 안전! 양자 취약성 비교 RSA 2048비트: Shor 알고리즘으로 다항 시간에 해독 → 양자 취약 ECC 256비트: Shor 알고리즘으로 다항 시간에 해독 → 양자 취약 AES-256: Grover로 128비트 보안 유지 → 양자 안전 ML-KEM/ML-DSA: 격자 문제 기반 → 양자 안전

ML-KEM (CRYSTALS-Kyber) — 키 캡슐화

ML-KEM(Module-Lattice Key Encapsulation Mechanism)은 키 교환에 사용되는 KEM입니다. TLS 1.3과 SSH에서 기존 X25519 키 교환의 대체/보완으로 도입되고 있습니다. ML-KEM-768이 128비트 보안 수준으로 가장 널리 배포됩니다.

LWE가 암호화를 가능하게 하는 원리 — 수치 예시

LWE 수식 b = As + e (mod q)에서 오차 e가 어떻게 보안을 제공하는지, 작은 수치로 직관을 잡아봅니다.

LWE 수치 예시 — 오차(e)가 보안을 만드는 과정 q = 17 (작은 소수), 비밀 s = 3 으로 단순화한 1차원 예시 1단계: 키 생성 (KeyGen) 비밀키: s = 3 (비공개) 공개 값 a = 7 (공개) 오차 없는 결과: a·s = 7×3 = 21 ≡ 4 (mod 17) 오차 추가: b = a·s + e = 4 + 2 = 6 (mod 17) 공개키: (a=7, b=6) e=2는 비공개 2단계: 공격자가 s를 찾으려는 시도 공격자는 a=7, b=6만 알고, s를 추측해야 합니다: s=0 → a·0=0, 오차=6 (큼) s=1 → a·1=7, 오차=16 (큼) s=2 → a·2=14, 오차=9 (큼) s=3 → a·3=4, 오차=2 (작음!) ✔ 1차원에서는 전수 탐색으로 "가장 작은 오차"를 찾을 수 있지만... 3단계: 차원이 높아지면? (ML-KEM: 256차원) 실제 ML-KEM-768: A는 768×768 행렬, s는 768차원 벡터, q=3329 각 s[i]가 0~3328 중 하나 → 후보 개수: 3329⁷⁶⁸ ≈ 2⁸⁹⁰⁰ (우주 원자 수의 2700배 이상) → 전수 탐색은 물론, 최선의 격자 공격도 2¹⁶⁰+ 연산 필요 (양자 컴퓨터로도 동일) 4단계: 비밀키 보유자는 어떻게 복호화하는가? 비밀키 s를 아는 사람: v - sᵗ·u = (메시지 + 작은 오차) → 반올림으로 오차 제거 → 메시지 복원 핵심: 오차 e가 충분히 작으면 반올림(rounding)으로 제거 가능 → 정확한 복호화 보장 (실패 확률 ≈ 2⁻¹⁶⁰)
ML-KEM 파라미터 세트 비교
파라미터NIST 보안 수준공개키암호문공유 비밀대응 고전 알고리즘
ML-KEM-5121 (~128비트)800B768B32BAES-128 동급
ML-KEM-7683 (~192비트)1,184B1,088B32BAES-192 동급 (권장)
ML-KEM-10245 (~256비트)1,568B1,568B32BAES-256 동급
ML-KEM 키 캡슐화 흐름:
  KeyGen():
    (A, t) = 공개키   -- A: 공개 행렬, t = As + e (mod q)
    s = 비밀키         -- 작은 계수 벡터

  Encaps(pk):
    r ← 랜덤
    (u, v) = 암호문    -- u = A^T·r + e₁, v = t^T·r + e₂ + ⌈q/2⌋·m
    K = 공유 비밀 (256비트)

  Decaps(sk, ct):
    m' = v - s^T·u     -- 오차 제거 → 메시지 복원
    K = 공유 비밀 (동일)

ML-KEM-768 파라미터: n=256, k=3, q=3329
  공개키: 1184 바이트, 암호문: 1088 바이트, 공유 비밀: 32 바이트
ML-KEM 키 캡슐화 프로토콜 흐름 Alice (수신자) KeyGen() → (pk, sk) pk: 1184B, sk: 2400B Bob (송신자) 공개키 pk 전송 (1184 바이트) Encaps(pk) → (ct, K) ct: 1088B, K: 32B 공유 비밀 암호문 ct 전송 (1088 바이트) Decaps(sk, ct) → K 동일한 32B 공유 비밀 복원 K = 공유 비밀 (256비트) K = 공유 비밀 (256비트) 양쪽 동일한 K 보유 → 대칭 암호 키로 사용
KEM vs 키 교환: 전통적인 Diffie-Hellman은 양쪽이 대칭적으로 값을 교환하는 반면, KEM(Key Encapsulation Mechanism)은 수신자가 키 쌍을 생성하고 송신자가 공개키로 비밀을 캡슐화하는 비대칭 구조입니다. TLS 1.3에서 ML-KEM은 기존 ECDHE 키 교환 자리에 동일한 방식으로 통합됩니다.

리눅스 커널 PQC 현황

2025년 현재, 리눅스 커널의 PQC 지원은 초기 단계입니다. 주요 진행 상황은 다음과 같습니다.

영역현황적용 시점
TLS (kTLS)사용자 공간 라이브러리(OpenSSL 3.5+)에서 ML-KEM 지원, kTLS는 대칭 암호만 오프로드진행 중
모듈 서명ML-DSA 서명 검증 패치(Patch) 논의 중미정
IPsec/IKEv2RFC 9370 (PQ 키 교환) 사용자 공간 지원진행 중
WireGuard하이브리드(X25519+ML-KEM) 제안 단계미정
dm-crypt/fscryptAES-256-XTS(양자 안전) 유지, 변경 불필요현재
FN-DSA (Falcon)NIST 추가 서명 표준(FIPS 206 초안), 격자 기반 컴팩트 서명표준화 중
HQC코드 기반 KEM (NIST 4라운드), ML-KEM 대안 후보평가 중
/* include/crypto/kem.h — KEM API 구조 (향후 커널 통합 시 예상 패턴) */
/* 현재 커널에는 akcipher(비대칭 암호) API가 존재하며,           */
/* ML-KEM 통합 시 아래와 유사한 KEM 전용 인터페이스가 추가될 것입니다 */
struct kem_alg {
    int (*generate_keypair)(struct crypto_kem *tfm,
                           u8 *pk, unsigned int *pk_len,
                           u8 *sk, unsigned int *sk_len);
    int (*encapsulate)(struct crypto_kem *tfm,
                       const u8 *pk, unsigned int pk_len,
                       u8 *ct, unsigned int *ct_len,
                       u8 *ss, unsigned int *ss_len);
    int (*decapsulate)(struct crypto_kem *tfm,
                       const u8 *sk, unsigned int sk_len,
                       const u8 *ct, unsigned int ct_len,
                       u8 *ss, unsigned int *ss_len);
    unsigned int pk_size;    /* ML-KEM-768: 1184 */
    unsigned int sk_size;    /* ML-KEM-768: 2400 */
    unsigned int ct_size;    /* ML-KEM-768: 1088 */
    unsigned int ss_size;    /* 공유 비밀:     32 */
    struct crypto_alg base;
};
/* crypto/testmgr.h — ML-KEM KAT 벡터 구조 (FIPS 203 기반 예시) */
struct kem_testvec {
    const u8 *seed;        /* d || z: 키 생성 시드 (64바이트) */
    const u8 *pk;          /* 기대 공개키 */
    const u8 *sk;          /* 기대 비밀키 */
    const u8 *ct;          /* 기대 암호문 */
    const u8 *ss;          /* 기대 공유 비밀 (32바이트) */
    const u8 *encaps_msg;  /* 캡슐화 난수 입력 (32바이트) */
    unsigned int pk_len;
    unsigned int sk_len;
    unsigned int ct_len;
};

/* NIST ACVP (Automated Cryptographic Validation Protocol) 테스트 벡터로 검증 */
/* 각 벡터는 seed → KeyGen → pk/sk 비교, msg → Encaps → ct/ss 비교,       */
/* sk + ct → Decaps → ss 일치를 확인합니다                                  */
하이브리드 접근: 현재 권장되는 전환 전략은 기존 알고리즘과 PQC 알고리즘을 동시에 사용하는 하이브리드 모드입니다. 예를 들어 TLS 1.3에서 X25519+ML-KEM-768을 결합하면, PQC 알고리즘에 미발견 취약점이 있더라도 X25519만으로 보안이 유지됩니다. Chrome과 Cloudflare는 이미 이 방식을 배포하고 있습니다.

ML-DSA (CRYSTALS-Dilithium) — 디지털 서명

ML-DSA(Module-Lattice Digital Signature Algorithm, FIPS 204)는 PQC 시대의 주력 디지털 서명 표준입니다. Module-LWE와 Module-SIS(Short Integer Solution) 문제에 기반하며, Fiat-Shamir with Aborts 패러다임을 사용합니다 — 서명 과정에서 비밀키 정보가 누출되지 않도록 출력의 통계적 분포를 검사하고, 조건에 맞지 않으면 서명을 폐기하고 재시도합니다. 이 "Aborts" 메커니즘이 ML-DSA의 핵심이며, 리눅스 커널 모듈 서명에 적용될 경우 기존 RSA/ECDSA 서명을 대체하게 됩니다.

왜 "Aborts"가 필요한가 — 일상 비유: 범인이 알리바이를 만들 때, "어제 뭐 했어?"라는 질문에 매번 같은 대답을 하면 의심받지 않습니다. 하지만 질문이 달라질 때마다 대답에 범인의 진짜 행적(비밀키)이 새어나오는 패턴이 생길 수 있습니다. ML-DSA의 Aborts는 이런 경우를 방지합니다 — 응답 벡터 z가 비밀키 s의 존재를 통계적으로 드러내는 경우(‖z‖이 너무 크거나 특정 패턴을 보이는 경우), 그 서명을 폐기하고 처음부터 다시 시도합니다. 평균적으로 ML-DSA-65에서는 약 4~7회 시도 후 유효한 서명이 생성됩니다.
Fiat-Shamir with Aborts — 거부 샘플링 직관 Aborts 없이 서명하면? 응답 z = y + c·s 에서: • y는 균일 분포 (무작위) • c·s 는 비밀키에 의존하는 치우침(bias) → z의 분포가 비밀키 s의 방향으로 치우침! 서명을 수백~수천 개 수집하면 통계 분석으로 s를 역추적할 수 있습니다. z 분포 → 비밀키 방향 치우침 ✘ 안전하지 않음 Aborts 적용 후 (거부 샘플링) 서명 생성 후 검사: • ‖z‖∞ ≥ γ₁-β 이면 → 폐기 후 재시도 • 힌트 벡터 조건 불충족 → 폐기 후 재시도 → 통과한 z만 출력 → 균일 분포와 구별 불가! 서명 수만 개를 수집해도 비밀키 s에 대한 정보를 얻을 수 없습니다. z 분포 → 완전 균일 (s 무관) ✔ 안전함
ML-DSA 서명/검증 흐름 (Fiat-Shamir with Aborts):
  KeyGen():
    (A, t) = 공개키     -- A: 공개 행렬, t = As₁ + s₂ (mod q)
    (s₁, s₂) = 비밀키   -- 작은 계수 벡터 쌍

  Sign(sk, msg):
    loop:                -- Aborts: 조건 불충족 시 반복
      y ← 균일 분포 샘플링
      w₁ = HighBits(Ay)         -- 커밋먼트 상위 비트
      c = H(w₁ || msg)          -- 도전값 (해시)
      z = y + c·s₁              -- 응답 벡터
      if ‖z‖∞ ≥ γ₁-β: restart  -- 비밀키 누출 방지 검사
      if ‖LowBits(Ay-c·s₂)‖∞ ≥ γ₂-β: restart
    return σ = (c̃, z, h)

  Verify(pk, msg, σ):
    w'₁ = UseHint(h, Az - ct)   -- 커밋먼트 복원
    c' = H(w'₁ || msg)          -- 도전값 재계산
    return c' == c̃ AND ‖z‖∞ < γ₁-β

ML-DSA-65 파라미터: k=6, l=5, q=8380417, γ₁=2¹⁹
  공개키: 1952B, 서명: 3293B, 비밀키: 4032B (NIST 보안 수준 3)
ML-DSA — Fiat-Shamir with Aborts 서명 흐름 서명 생성 (Sign) ① y ← 균일 분포 샘플링 마스킹 벡터 생성 ② w₁ = HighBits(Ay) 커밋먼트 계산 ③ c = H(w₁ ‖ msg) 도전값 생성 ④ z = y + c·s₁ 응답 벡터 계산 ⑤ ‖z‖∞ ≥ γ₁-β ? 비밀키 누출 검사 실패 → ① 재시도 (Abort) σ = (c̃, z, h) 서명 검증 (Verify) ① w'₁ = UseHint(h, Az - ct) 커밋먼트 복원 ② c' = H(w'₁ ‖ msg) 도전값 재계산 ③ c' == c̃ ? AND ‖z‖∞ < γ₁-β 유효 (PASS) 무효 (FAIL)
ML-DSA 파라미터 세트 비교
파라미터NIST 보안 수준공개키서명 크기비밀키대응 고전 알고리즘
ML-DSA-442 (~128비트)1,312B2,420B2,560BECDSA P-256, Ed25519
ML-DSA-653 (~192비트)1,952B3,293B4,032BECDSA P-384
ML-DSA-875 (~256비트)2,592B4,595B4,896BECDSA P-521
커널 모듈 서명과 ML-DSA: 리눅스 커널의 모듈 서명 검증(CONFIG_MODULE_SIG_FORCE)은 현재 RSA-4096/ECDSA를 사용합니다. ML-DSA 전환 시 서명 크기가 수십 배 증가하지만(ECDSA 64B → ML-DSA-65 3,293B), 모듈 서명은 한 번만 검증하므로 성능 영향은 미미합니다. 핵심은 공급망 보안(Supply Chain Security)에서의 양자 내성 확보이며, 자세한 내용은 공급망 보안 페이지(Page)를 참고하십시오.

SLH-DSA (SPHINCS+) — 해시 기반 서명

SLH-DSA(Stateless Hash-Based Digital Signature Algorithm, FIPS 205)는 격자 수학에 의존하지 않고 오직 해시 함수의 보안 속성(충돌 저항성, 프리이미지 저항성)에만 기반하는 서명 방식입니다. 격자 기반 ML-DSA에 비해 서명 크기가 수~수십 배 크지만, 격자 문제에 대한 미래의 수학적 돌파에도 영향받지 않으므로 보수적 대안(conservative fallback)으로 표준화되었습니다.

SLH-DSA는 내부적으로 FORS(Forest of Random Subsets) 서명과 XMSS(eXtended Merkle Signature Scheme) 유사 구조를 결합하여 무상태(stateless) 서명을 구현합니다. "fast" 변형(f)은 서명 생성이 빠르지만 크기가 크고, "small" 변형(s)은 크기가 작지만 생성이 느립니다.

일상 비유 — 나무와 나뭇잎: SLH-DSA의 핵심 구조인 머클 트리(Merkle Tree)를 나무에 비유할 수 있습니다. 나뭇잎 하나하나가 일회용 서명 키(OTS)이고, 나뭇잎들의 해시를 쌍으로 묶어 올라가면 하나의 뿌리(Root)가 됩니다. 이 뿌리가 공개키입니다. 서명을 검증할 때는 나뭇잎에서 뿌리까지의 "해시 경로"만 따라가면 되므로, 나무 전체를 보지 않아도 진위를 확인할 수 있습니다. 서명 크기가 큰 이유는 이 인증 경로(형제 노드 해시들)를 모두 포함해야 하기 때문입니다.
SLH-DSA 내부 구조 — 머클 트리와 FORS/WOTS+ Hypertree (머클 트리 계층 구조) Root = 공개키 H(h₀ ‖ h₁) H(h₂ ‖ h₃) h₀ h₁ h₂ h₃ WOTS+ #0 WOTS+ #1 WOTS+ #2 WOTS+ #3 FORS (메시지 서명) 구성 요소 설명 Root (공개키) 모든 OTS 키를 대표하는 단일 해시값 32~64바이트의 매우 작은 공개키 내부 노드 (해시 쌍) 자식 두 노드의 해시를 결합 변조 시 루트까지 연쇄 변경 → 감지 WOTS+ (일회용 서명) Winternitz One-Time Signature 각 나뭇잎에 고유한 일회용 서명 키 FORS (Few-Time Signature) 메시지 해시에 따라 서브트리 선택 무상태: 메시지 해시가 인덱스 결정 서명 = FORS 서명 + 인증 경로 나뭇잎→루트 경로의 형제 노드 해시
SLH-DSA 파라미터 세트 (FIPS 205)
파라미터보안 수준공개키서명 크기서명 생성특성
SLH-DSA-128f1 (~128비트)32B17,088B빠름빠른 서명, 큰 크기
SLH-DSA-128s1 (~128비트)32B7,856B느림작은 서명, 느린 생성
SLH-DSA-192f3 (~192비트)48B35,664B빠름빠른 서명, 큰 크기
SLH-DSA-192s3 (~192비트)48B16,224B느림작은 서명, 느린 생성
SLH-DSA-256f5 (~256비트)64B49,856B빠름빠른 서명, 큰 크기
SLH-DSA-256s5 (~256비트)64B29,792B느림작은 서명, 느린 생성
격자 암호의 보험 정책: SLH-DSA는 "만약 격자 기반 암호(ML-KEM, ML-DSA)에 대한 새로운 공격이 발견된다면?"이라는 질문에 대한 답입니다. 해시 함수(SHA-256, SHAKE)의 보안성은 수십 년간 검증되었으며, Grover 알고리즘으로도 충분한 보안 수준이 유지됩니다. XMSS(RFC 8391)와 LMS(RFC 8554)는 상태 관리가 필요한 유사 해시 기반 서명으로, 펌웨어 서명처럼 상태 추적이 가능한 환경에서 이미 사용 가능합니다.

하이브리드 키 교환 (X25519 + ML-KEM-768)

PQC 전환기의 표준 전략은 하이브리드 키 교환입니다. 고전 알고리즘(X25519)과 PQC 알고리즘(ML-KEM-768)을 동시에 실행하여, 둘 중 하나라도 안전하면 세션 키가 보호됩니다. TLS 1.3에서는 X25519MLKEM768(0x11EC) named group으로 표준화되어 있으며, Chrome 131+와 Firefox 132+에서 기본 활성화되었습니다.

하이브리드 키 교환 — X25519 + ML-KEM-768 클라이언트 서버 트랙 1: X25519 (고전 ECDH) x25519_keypair → pk_c (32B) ClientHello key_share (32B) x25519_compute → SS₁ (32B) 트랙 2: ML-KEM-768 (포스트 양자 KEM) ML-KEM KeyGen → pk (1184B) ClientHello key_share (1184B) Encaps(pk) → ct, SS₂ (32B) ServerHello key_share: pk_s (32B) ServerHello key_share: ct (1088B) HKDF(SS₁ ‖ SS₂) → 세션 키 두 공유 비밀을 연결하여 키 유도 → X25519 또는 ML-KEM 중 하나만 안전해도 세션 보호 총 오버헤드: ClientHello +1216B, ServerHello +1120B (기존 X25519 대비)
/* 하이브리드 KDF 결합 패턴 — X25519 + ML-KEM-768 */
/* TLS 1.3 스타일: 두 공유 비밀을 연결 후 HKDF로 키 유도 */
static int hybrid_derive_secret(u8 *session_key,
                                 const u8 *x25519_ss, size_t x25519_len,
                                 const u8 *mlkem_ss, size_t mlkem_len)
{
    u8 combined[64]; /* 32(X25519) + 32(ML-KEM) */
    struct crypto_shash *hmac;
    int err;

    /* 두 공유 비밀을 순서대로 연결 */
    memcpy(combined, x25519_ss, x25519_len);          /* SS₁ */
    memcpy(combined + x25519_len, mlkem_ss, mlkem_len); /* SS₂ */

    /* HKDF-SHA-256으로 세션 키 유도 */
    hmac = crypto_alloc_shash("hmac(sha256)", 0, 0);
    if (IS_ERR(hmac))
        return PTR_ERR(hmac);

    err = hkdf_extract(hmac, combined, sizeof(combined),
                        salt, salt_len, session_key);
    crypto_free_shash(hmac);
    memzero_explicit(combined, sizeof(combined));
    return err;
}

PQC 성능 및 키 크기 비교

PQC 알고리즘은 양자 안전성을 제공하는 대신 키와 서명 크기가 크게 증가합니다. 아래 표는 고전 알고리즘과 PQC 알고리즘의 연산 성능과 크기를 비교합니다. ML-KEM은 기존 X25519와 비슷한 연산 속도를 보이지만, ML-DSA 서명은 Ed25519에 비해 크기가 50배 이상입니다.

고전 vs PQC 알고리즘 성능 비교 (x86-64 참고치)
알고리즘유형KeyGen서명/캡슐화검증/역캡슐화(Decapsulation)양자 안전
RSA-2048서명~4 op/s~1,000 op/s~35,000 op/s아니오
ECDSA P-256서명~20,000 op/s~20,000 op/s~8,000 op/s아니오
Ed25519서명~30,000 op/s~30,000 op/s~12,000 op/s아니오
X25519KEM/DH~30,000 op/s~30,000 op/s아니오
ML-KEM-768KEM~45,000 op/s~55,000 op/s~60,000 op/s
ML-DSA-65서명~8,000 op/s~4,000 op/s~8,000 op/s
SLH-DSA-128f서명~300 op/s~60 op/s~1,200 op/s
키 · 서명(암호문) 크기 비교 (바이트, 로그 스케일) 공개키(좌측 막대) / 서명 또는 암호문(우측 막대) Ed25519 ECDSA P-256 RSA-2048 ML-KEM-768 ML-DSA-65 SLH-DSA-128f pk 32B sig 64B pk 33B sig 64B pk 256B sig 256B pk 1,184B ct 1,088B pk 1,952B sig 3,293B pk 32B sig 17,088B 공개키 서명/암호문
네트워크 영향: TLS 1.3 하이브리드 핸드셰이크(X25519+ML-KEM-768)는 ClientHello에 ~1,216바이트, ServerHello에 ~1,120바이트가 추가됩니다. MTU(1500B)를 초과하여 TCP 분할이 발생할 수 있으며, 이는 NGFW의 SNI 추출에 영향을 줄 수 있습니다. 자세한 내용은 NGFW PQC 전환 페이지를 참고하십시오.

커널 암호화 자체 검증 (Crypto Self-Test)

암호화 구현의 사소한 버그는 치명적인 결과를 초래할 수 있습니다 — AES 라운드 하나가 빠지면 보안이 크게 약화되지만, 입출력(I/O) 형식은 정상이므로 기능 테스트로는 발견하기 어렵습니다. 리눅스 커널은 이 문제를 해결하기 위해, 암호화 알고리즘이 시스템에 등록될 때 자동으로 셀프테스트(Self-Test)를 실행합니다. 이 메커니즘은 crypto/testmgr.c에 구현되어 있으며, 알려진 답 테스트(KAT, Known Answer Test) — NIST/RFC 공식 테스트 벡터와 출력을 비교 — 를 통해 구현의 정확성을 검증합니다. 테스트를 통과하지 못한 알고리즘은 시스템에 등록되지 않습니다.

testmgr 동작 원리

커널 Crypto 셀프테스트 — 알고리즘 등록 시 자동 검증 부팅 시 자동 실행 crypto_register_alg() 드라이버 초기화 시 alg_test() crypto/testmgr.c KAT: 알려진 입력 → 기대 출력 비교 교차 검증: Generic vs HW 구현 비교 PASS → 등록 완료 FAIL → 등록 거부 testmgr 주요 테스트 유형 KAT (Known Answer Test) NIST/RFC 공식 테스트 벡터 사용 AES: FIPS 197 Appendix 벡터 SHA-256: FIPS 180-4 벡터 교차 검증 (Fuzz Test) 랜덤 입력으로 Generic ↔ HW 구현 비교 aes-generic vs aes-aesni 결과 일치 확인 CONFIG_CRYPTO_MANAGER_EXTRA_TESTS 활성화 시
/* crypto/testmgr.c — 셀프테스트 구조체 (AES-GCM 예시) */
static const struct aead_testvec aes_gcm_tv[] = {
    { /* NIST SP 800-38D 테스트 벡터 */
        .key    = "\xfe\xff\xe9\x92\x86\x65\x73\x1c"
                  "\x6d\x6a\x8f\x94\x67\x30\x83\x08",
        .klen   = 16,
        .iv     = "\xca\xfe\xba\xbe\xfa\xce\xdb\xad"
                  "\xde\xca\xf8\x88",
        .input  = "\xd9\x31\x32\x25\xf8\x84\x06\xe5"
                  /* ... 평문 데이터 ... */,
        .result = "\x42\x83\x1e\xc2\x21\x77\x74\x24"
                  /* ... 기대 암호문+태그 ... */,
    },
    /* ... 추가 테스트 벡터들 ... */
};

/* 테스트 실행: alg_test("gcm(aes)", ...) */
/* 1. 키 설정 → 2. 암호화 → 3. 결과 비교 → 4. 복호화 → 5. 역검증 */
/* 한 벡터라도 불일치하면 WARN_ONCE() + 알고리즘 등록 거부 */

FIPS 140 모드

CONFIG_CRYPTO_FIPS=y로 빌드하면, 커널은 FIPS 140 인증 모드로 동작합니다. 이 모드에서는 셀프테스트 실패 시 알고리즘을 등록하지 않을 뿐만 아니라, 부팅 시 무결성 검사도 수행합니다. fips_enabled 전역 변수가 1이면 비승인 알고리즘(MD5 등)의 사용이 제한됩니다.

/* crypto/testmgr.c — FIPS 모드 테스트 강제 */
static int alg_test(const char *driver, const char *alg,
                    u32 type, u32 mask)
{
    int i = alg_find_test(alg);  /* 테스트 벡터 검색 */

    if (i < 0) {
        if (fips_enabled) {
            /* FIPS 모드: 테스트 벡터 없는 알고리즘 사용 금지 */
            pr_err("alg: %s has no test vectors in FIPS mode\n", alg);
            return -EINVAL;
        }
        return 0;  /* 비FIPS: 테스트 없이 통과 */
    }

    return alg_test_descs[i].test(alg_test_descs + i, driver, type, mask);
}
성능 영향: 셀프테스트는 알고리즘 최초 등록 시에만 실행되며, 이후 암호화 연산에는 영향이 없습니다. CONFIG_CRYPTO_MANAGER_EXTRA_TESTS를 활성화하면 추가 퍼지 테스트(랜덤 입력 교차 검증)가 실행되어 부팅 시간이 수 초 증가하지만, HW 가속 구현의 버그를 효과적으로 검출합니다.

알고리즘 종합 비교

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

보안 강도 수준 비교

암호학적 보안 강도는 "보안 비트(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 터널(Tunnel)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

FIPS 203/204/205가 2024년 8월 확정된 뒤, 2025-2026년은 "표준이 나왔으니 이제 정책과 구현이 따라가는" 단계입니다. 알고리즘 자체의 수학은 더 이상 크게 움직이지 않지만, 규제 달력(Calendar)과 커널 업스트림 병합 일정이 실제 배치(Deployment)를 결정합니다.

시점변경알고리즘 관점 영향
2024-08-13FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA) 최종표준 번호가 확정되어 OID, 식별자, 파라미터 집합의 명명이 고정됩니다.
2024-10HQC가 NIST 4라운드 백업 KEM으로 선정격자 기반(ML-KEM)에 문제가 발견될 경우를 대비한 코드 기반 KEM이 확보되어, 하이브리드 구성에 선택지가 늘었습니다.
2025-03-04NSA CNSS Policy 15 (CNSA 2.0)국가 안보 계열에서 ML-KEM-1024, ML-DSA-87, LMS, XMSS, SHA-384/512, AES-256을 필수로 규정.
2025 진행 중NIST SP 800-227 (KEM 구성), SP 800-232 (서명 구성)FIPS 203/204를 실제로 프로토콜에서 쓸 때의 파라미터 선택과 키 바인딩 규칙을 추가 지침으로 정리합니다.
2025-04-08OpenSSL 3.5 LTSML-KEM, ML-DSA, SLH-DSA를 기본 provider에 내장. TLS 1.3 기본 key_share가 X25519MLKEM768, X25519.
2026 예상FN-DSA (Falcon, FIPS 206 초안) 최종격자 기반 "짧은 서명" 보완책으로, ML-DSA와는 다른 트레이드오프를 가집니다.
2026-09-21FIPS 140-2 Historical 이관SHA-1 기반 HMAC DRBG 등 구식 구성이 시장에서 사실상 사라집니다.
2030NIST SP 800-131A Rev.3 발효SHA-1, SHA-224, AES-ECB 사용 전면 금지. 레거시 호환용 경로만 제한적으로 허용됩니다.

CNSA 2.0 요구 알고리즘 집합

용도CNSA 2.0 요구커널/라이브러리 현황 (2026-04)
대칭 암호AES-256 (CBC/GCM/CTR)커널 Crypto API 기본, VAES 기반 고속 경로 6.15 이후 제공
해시SHA-384, SHA-512커널 shash / lib/crypto 양쪽 제공. SHA-256은 일반 용도에 한해 계속 사용
공개키 키 캡슐화ML-KEM-1024OpenSSL 3.5 내장, 커널은 미구현. 하이브리드 X25519MLKEM768는 전환기 사용
디지털 서명ML-DSA-87OpenSSL 3.5 내장, 커널 모듈 서명에는 6.19 이후 진입 예상
상태 기반 서명LMS, XMSSOpenSSL 3.4 이후 기본 지원, 펌웨어 서명 용도로 사용 가능. 커널 asymmetric 키는 미지원
난수 생성NIST SP 800-90A/B/C 조합커널 CRNG가 요구를 충족. vgetrandom() vDSO가 사용자 공간 경로를 가속

하이브리드 키 교환 성능 스냅샷 (2026)

하이브리드 TLS 구성의 핵심 수치는 (1) 핸드셰이크 RTT, (2) ClientHello 크기, (3) 서버 CPU 소모입니다. 벤치마크 수치는 CPU 세대와 OpenSSL 프로바이더에 따라 편차가 크지만, 대체적인 기준선은 다음과 같습니다.

키 교환공개키 크기ClientHello 증분핸드셰이크 CPU (서버, 상대값)
X2551932 B기준1.0×
ML-KEM-7681,184 B+1.2 KB1.1× (AVX2 최적화 적용 시)
X25519MLKEM768 (하이브리드)1,216 B+1.3 KB1.15×
ML-KEM-10241,568 B+1.6 KB1.3×
P-384 (비교용)97 B+0.1 KB2.0× 이상 (일반 ECDH 대비)
서명서명 크기검증 속도 (상대값, x86-64)비고
ECDSA P-256~70 B1.0×기준
Ed2551964 B1.2×결정론적, 안전한 기본
ML-DSA-653,293 B0.4×CNSA 2.0 하한 파라미터
ML-DSA-874,627 B0.25×CNSA 2.0 권장 파라미터
SLH-DSA-128f~17 KB0.05×해시 기반, 격자 가정 불필요
LMS (L=10/W=4)~1.7 KB0.2×상태 기반, 펌웨어 서명 특화

커널 업스트림 PQC 상태 요약

  • crypto/ — ML-KEM, ML-DSA, SLH-DSA의 네이티브 구현은 아직 병합되지 않았습니다. 대부분의 실제 PQC 처리는 사용자 공간 OpenSSL 3.5, liboqs가 담당합니다.
  • asymmetric_keys/ — X.509 파서와 서명 검증기에 ML-DSA OID를 수용하는 준비 패치(Patch)가 2025년 하반기부터 리뷰되고 있습니다.
  • module signing — Linux 6.19 대상 패치 시리즈가 ML-DSA-65/87 기반 모듈 서명을 추가하는 방향으로 정돈되고 있습니다. 업스트림 첫 PQC 검증기 진입점(Entry Point)이 이 경로일 가능성이 높습니다.
  • fscrypt / dm-crypt — 대칭 경로 자체는 변하지 않지만, 장기 아카이브를 위한 KEM 래핑 재설계가 DebConf25에서 논의되었습니다. 2026-2027 RFC 시리즈가 예상됩니다.
  • TPM 2.0 — TCG의 PQC 확장(LDAA 등)은 아직 주요 벤더 펌웨어에 내려오지 않아, 커널 drivers/char/tpm/ 경로는 전통적인 RSA/ECDSA가 유지됩니다.
운영 관점 요약: 2026년 현재 커널이 직접 PQC 연산을 하는 지점은 사실상 없습니다. 커널이 맡는 역할은 (1) CRNG로 엔트로피 공급, (2) KTLS/IPsec 레코드 계층 대칭 암호 처리, (3) 모듈 서명/IMA 검증기라는 경계입니다. 이 세 축이 2026-2027년 사이 PQC 영향권으로 차례로 들어옵니다. 알고리즘 계층 공부는 OpenSSL 3.5, liboqs를 중심으로, 커널 쪽 학습은 포스트 양자 보안 전환, 키링(Keyring), 커널 모듈 서명을 함께 읽는 편이 시간 투자 대비 효율적입니다.

외부 참고 자료

NIST 표준 및 가이드

IETF RFC

학술 논문 및 설계자 문서

리눅스 커널 문서 및 소스