Quipu-Log 교과서
파트 6 · 기밀성: 비밀을 지키면서 검색하기 (보안 ②)

25 · 대칭·비대칭·하이브리드 암호와 AEAD

앞 장에서 Rsa 보호 수준이 "AES-256-GCM으로 암호화하고 RSA-OAEP로 키를 감싼다"고 짧게 언급했습니다. 그런데 왜 RSA로 직접 암호화하지 않고 이런 두 단계를 거칠까요? 이 챕터에서는 대칭·비대칭·하이브리드 암호의 차이를 직관적으로 쌓고, AEAD가 암호화에 무결성까지 더하는 이유를 살펴봅니다. Quipu-Log의 crypto.rs가 이 이론을 어떻게 구현하는지도 함께 확인합니다.

한 문장 요약

데이터는 빠른 AES-256-GCM(대칭)으로 암호화하고, 그 비밀 키만 RSA 공개키(비대칭)로 감쌉니다. 이것이 하이브리드 키 래핑입니다 — 각자가 잘하는 일을 맡는 것이죠.

먼저: 대칭 암호 vs 비대칭 암호

두 개념의 핵심 차이부터 잡아봅시다.

대칭 암호(Symmetric)는 암호화와 복호화에 같은 키를 씁니다. AES가 대표적입니다. 빠르고 데이터 크기에 제한이 없습니다. 단점은 "공유"입니다 — 상대방에게 키를 어떻게 안전하게 전달하느냐가 문제죠. 서버와 클라이언트가 같은 비밀 키를 알고 있어야 하는데, 처음 만나는 사이에 안전한 채널이 없다면 키 공유 자체가 난제입니다.

비대칭 암호(Asymmetric)는 공개키와 개인키 쌍을 씁니다. 공개키로 암호화 → 개인키로만 복호화. RSA가 대표적입니다. 공개키는 이름처럼 공개해도 됩니다 — 누가 봐도 상관없습니다. 덕분에 "어떻게 키를 안전하게 전달하나"라는 문제를 해결합니다. 단점은 느리고 암호화할 수 있는 데이터 크기에 제한이 있다는 점입니다(RSA 2048비트 = 최대 약 245바이트). 수백 킬로바이트 파일을 RSA로 직접 암호화하는 건 불가능에 가깝습니다.

비유

공개키를 잠긴 편지함이라고 생각해 보세요. 누구나 편지를 넣을 수 있지만(=공개키로 암호화), 꺼낼 수 있는 건 열쇠(=개인키)를 가진 주인뿐입니다. 편지함 자체는 공개 장소에 두어도 됩니다.

왜 RSA로 직접 암호화하지 않나 — 하이브리드 키 래핑의 탄생

두 암호의 장단점을 나란히 놓으면 최적 전략이 보입니다.

대칭(AES)비대칭(RSA)
속도매우 빠름수십~수백 배 느림
크기 제한없음있음 (RSA 2048 ≈ 245바이트)
키 공유 문제있음없음 (공개키 공개 가능)

해결책은 두 방법의 장점을 조합하는 것입니다. 데이터 자체는 대칭(AES)으로 암호화하고, 그 대칭 키(DEK, Data Encryption Key)만 비대칭(RSA)으로 감쌉니다. 이것이 하이브리드 키 래핑(Hybrid Key Wrapping)입니다.

평문 데이터 "Alice Smith" AES-256-GCM 데이터 암호화 (빠름) 암호문 ciphertext + nonce DEK (32바이트) 랜덤 AES 키 키 사용 RSA-OAEP DEK 암호화 (느리지만 32B만) wrapped_key RSA 암호문 (b64) RSA 공개키 서버가 보유 가능 디스크 저장 ciphertext nonce wrapped_key key_version
데이터는 빠른 AES-256-GCM으로 암호화, DEK(AES 키)만 RSA-OAEP로 감싸 저장. 디스크에는 암호문·nonce·wrapped_key·key_version 네 조각이 남는다.

실제 코드에서 이 흐름이 정확히 어떻게 구현되는지 확인해 봅시다.

crates/quipu-core/src/crypto.rs — KeyRing::protect (Rsa 분기)// 1. DEK(Data Encryption Key): 32바이트 랜덤 대칭 키 생성
let mut dek = [0u8; 32];
rand::thread_rng().fill_bytes(&mut dek);
let mut nonce = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce);

// 2. 데이터를 AES-256-GCM으로 암호화
let cipher = Aes256Gcm::new_from_slice(&dek).expect("32-byte key");
let ciphertext = cipher.encrypt(Nonce::from_slice(&nonce), value.canonical_bytes().as_slice())?;

// 3. DEK를 RSA 공개키(OAEP 패딩)로 감싸기 — DEK는 32바이트라 RSA 크기 제한 문제 없음
let wrapped_key = key.encrypt(&mut rand::thread_rng(), Oaep::new::<Sha256>(), &dek)?;

DEK는 32바이트뿐이므로 RSA 크기 제한에 걸리지 않습니다. 대용량 데이터가 있어도 AES로 빠르게 처리합니다. 각 기록마다 랜덤 DEK와 nonce를 새로 생성하기 때문에 같은 값을 두 번 암호화해도 매번 다른 암호문이 나옵니다.

AEAD: 암호화 + 변조 탐지를 한번에

AES-256-GCM은 단순한 암호화 알고리즘이 아닙니다. AEAD(Authenticated Encryption with Associated Data) 계열입니다. GCM = Galois/Counter Mode인데, 이 모드의 핵심 특성은 인증 태그(authentication tag)를 함께 생성한다는 점입니다.

작동 원리를 간단히 설명하면 이렇습니다. 암호화할 때 암호문 + 인증 태그를 함께 출력합니다. 복호화할 때 인증 태그를 먼저 검증합니다. 태그가 맞지 않으면, 즉 암호문이 단 한 비트라도 수정됐다면 복호화 자체가 실패합니다.

보안 포인트

AEAD의 핵심: 암호화된 데이터를 누군가 수정하면 복호화가 실패합니다. 디스크에 저장된 암호문을 공격자가 1비트 뒤집어도 decrypt()는 에러를 반환합니다 — 조용히 손상된 값을 돌려주지 않습니다. 이것은 파트 5에서 다룬 변조 탐지(머클 트리)와는 독립적인 암호학적 보장입니다. 두 방어가 겹쳐서 작동합니다.

실제로 테스트 코드에서 이 동작이 검증됩니다.

crates/quipu-core/src/crypto.rs — 변조 탐지 테스트let mut ct = b64::decode(&ciphertext).unwrap();
ct[0] ^= 1; // 첫 바이트를 1비트 반전
let tampered = StoredValue::Rsa { key_version, wrapped_key, nonce,
    ciphertext: b64::encode(&ct) };
assert!(ring.decrypt(&tampered).is_err(), "GCM must reject a flipped bit");

HMAC — 키 있는 해시

HMAC은 "Hash-based Message Authentication Code"입니다. 키 없는 SHA-256과의 차이를 명확히 해봅시다.

SHA-256HMAC-SHA-256
없음비밀 키 필수
같은 입력 → 같은 출력?항상같은 키를 쓸 때만
오프라인 사전 공격가능(공개 알고리즘)키 없이 불가
용도중간 엔트로피 값 정도저엔트로피 값(SSN 등)

HMAC은 키를 데이터에 섞어 해싱합니다. 그래서 서버가 가진 키 없이는 같은 SSN이 있어도 다이제스트를 복제할 수 없습니다.

crates/quipu-core/src/crypto.rs — HMAC 계산type HmacSha256 = Hmac<Sha256>;

pub fn hmac_hex_with(&self, version: KeyVersion, data: &[u8]) -> Result<String> {
    let mut mac = <HmacSha256 as Mac>::new_from_slice(self.mac_of(version)?)
        .map_err(|e| Error::Crypto(e.to_string()))?;
    mac.update(data);
    Ok(hex(&mac.finalize().into_bytes()))
}

RSA-OAEP: 안전한 비대칭 암호화 패딩

Quipu-Log는 RSA 암호화에 OAEP(Optimal Asymmetric Encryption Padding) 패딩을 사용합니다. 옛날 방식인 PKCS#1 v1.5 암호화 패딩에는 알려진 취약점(Bleichenbacher 공격)이 있어서 현대 RSA 암호화에는 OAEP를 씁니다.

단, 서명(체크포인트 서명)에는 PKCS#1 v1.5를 씁니다 — 서명은 결정론적이어야 해서(같은 데이터 → 같은 서명) 같은 이유로 비결정론적 PSS 대신 PKCS#1 v1.5 서명을 선택했습니다. 암호화용 OAEP와 서명용 PKCS#1 v1.5, 두 가지 RSA 패딩이 각자의 역할에 맞게 사용됩니다.

KeyRing이 이 모든 것을 관리한다

KeyRing은 RSA 키쌍과 HMAC 키를 버전별로 관리하는 구조체입니다. 가장 높은 버전이 새 쓰기에 쓰이는 활성 키이고, 낮은 버전은 이전 레코드를 읽는 데 유지됩니다.

crates/quipu-core/src/crypto.rs — KeyRing 구조pub struct KeyRing {
    rsa: BTreeMap<KeyVersion, RsaPair>,    // 버전 → (공개키, 개인키?)
    macs: BTreeMap<KeyVersion, Vec<u8>>,  // 버전 → HMAC 키 바이트
}
// 활성 키: keys().next_back() — BTreeMap은 오름차순, 가장 높은 버전

쓰기 전용 배포에서는 RsaPairprivateNone입니다. 공개키만 있으면 암호화(쓰기)는 가능하지만 복호화(읽기)는 불가능합니다. 이것이 다음 챕터의 핵심 주제입니다 — 27장 쓰기 전용 배포.

정리

  • 대칭(AES)은 빠르지만 키 공유가 문제. 비대칭(RSA)은 키 공유가 쉽지만 느리고 크기 제한이 있음.
  • 하이브리드: 데이터는 AES-256-GCM으로, DEK만 RSA-OAEP로 감싼다 — 두 방법의 장점을 모두 취함.
  • AES-256-GCM은 AEAD — 암호화와 변조 탐지를 한번에. 1비트 변조도 복호화 실패로 드러남.
  • HMAC은 키 있는 해시 — 키 없이는 오프라인 사전 공격 불가. SHA-256의 저엔트로피 취약점을 해소.
  • KeyRing이 버전별 RSA+HMAC 키를 관리. 높은 버전 = 활성 키, 낮은 버전 = 이전 레코드 읽기용.
스스로 확인

① "RSA로 직접 데이터를 암호화하면 안 되는 이유" 두 가지를 말해 보세요.
② AES-GCM이 단순한 AES-CTR보다 나은 이유는 무엇인가요? (힌트: AEAD)
③ 같은 평문에 대해 HMAC 값은 항상 같을까요, 다를까요? SHA-256과 비교해서 설명해 보세요.