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

28 · 키 관리: 키링·버저닝·로테이션 vs 리키

암호화를 도입한 순간, 새로운 책임이 생깁니다 — 키 관리. 감사 로그에서 키를 잃는다는 건 DB에서 잊어버린 비밀번호와 다릅니다. 키를 잃으면 그 키로 보호된 모든 과거 기록을 영영 읽을 수 없게 됩니다. 에스크로도, 백도어도, 복구 방법도 없습니다. 이 챕터에서는 키링 버전 체계와 로테이션·리키 두 운영 경로를 정리합니다.

한 문장 요약

키 로테이션(새 키 추가)은 저렴하고 일상적입니다. 리키(old→new 재암호화)는 키 유출 후 딱 한 번, 비싸고 무겁습니다. 혼동하지 마세요.

KeyRing: 버전 키 관리자

KeyRing은 RSA 키쌍과 HMAC 키를 버전 번호로 관리합니다. 버전은 u32이고 0은 "키 없음" 센티넬로 예약돼 있어, 실제 키 버전은 1부터 시작합니다.

crates/quipu-core/src/crypto.rs — KeyVersion, KEYLESSpub type KeyVersion = u32;
pub const KEYLESS: KeyVersion = 0; // SHA-256 보호 필드의 다이제스트에 쓰는 센티넬

pub struct KeyRing {
    rsa: BTreeMap<KeyVersion, RsaPair>,  // BTreeMap: 버전 오름차순 정렬
    macs: BTreeMap<KeyVersion, Vec<u8>>,
}
// 활성 키 = 가장 높은 버전: BTreeMap::keys().next_back()

새 레코드를 쓸 때는 항상 가장 높은 버전(활성 키)이 사용됩니다. 저장된 레코드는 어떤 버전의 키로 만들었는지를 key_version 필드에 기록합니다. 읽을 때는 그 버전의 키를 꺼내씁니다.

시간 키 v1 활성 (쓰기+읽기) 로테이션 (v2 추가) 키 v1 → 읽기 전용 키 v2 활성 (쓰기+읽기) 로테이션 (v3 추가) v1, v2 → 읽기 전용 키 v3 활성 (쓰기+읽기) 레코드 A key_v = 1 레코드 B key_v = 2 레코드 C key_v = 3 * 레코드 A(key_v=1) 읽을 때: KeyRing에서 v1 키 꺼내 복호화. 로테이션 후에도 옛 레코드 읽기 가능. * 로테이션 = 새 버전 키 추가만. 옛 키 삭제하지 않음. 리키 없이 모든 구버전 레코드 계속 접근 가능.
키 로테이션은 새 버전을 추가하는 것. 이전 버전은 읽기용으로 유지된다. 레코드에 key_version이 기록돼 있어 어떤 키로 만들었는지 항상 알 수 있다.

로테이션 — 저렴하고 일상적

정기적인 키 교체(로테이션)는 보안 위생의 기본입니다. Quipu-Log에서 로테이션은 단순합니다: 새 버전의 키를 KeyRing에 추가하기만 하면 됩니다. 기존 레코드를 건드릴 필요가 없습니다.

quipu-server config.json — 버전 목록으로 전환"keys": {
    "hmac_keys": [
        { "version": 1, "file": "/etc/quipu/hmac-v1.key" },
        { "version": 2, "file": "/etc/quipu/hmac-v2.key" }  // 새 활성 키
    ],
    "rsa_keys": [
        { "version": 1, "public_key_pem_file": "/etc/quipu/rsa-v1-pub.pem" },
        { "version": 2, "public_key_pem_file": "/etc/quipu/rsa-v2-pub.pem" }
    ]
}

서버 재시작 후 v2가 활성 키가 됩니다. 새 레코드는 v2로 암호화됩니다. v1으로 만든 옛 레코드는 v1 키가 KeyRing에 남아 있으므로 여전히 읽을 수 있습니다. 아주 간단합니다.

왜 이렇게?

레코드에 key_version을 기록하는 이유가 여기에 있습니다. "이 레코드를 만들 때 어떤 키를 썼나"를 레코드 자체가 알려주므로, KeyRing에 여러 버전이 있어도 올바른 키를 자동으로 골라 씁니다. 버전 없이는 "어떤 키로 만들었나?"를 알 방법이 없어서 로테이션이 불가능합니다.

리키 — 키 유출 후의 비상 수단

일반적인 로테이션과 달리 리키(re-key)는 키 유출이 발생했을 때의 대응입니다. 유출된 RSA 개인키로는 이미 저장된 모든 암호문을 복호화할 수 있습니다. 이를 막으려면 저장된 레코드들을 새 키로 다시 암호화해야 합니다.

로테이션리키
언제정기적 (분기, 연간 등)키 유출 후
하는 일새 키 버전 추가전체 레지스트리 재암호화
비용저렴 (설정 변경만)비쌈 (전체 레코드 읽기+쓰기)
다운타임없음 (재시작만)있음 (오프라인 단독 실행)
결과새 레코드만 새 키 사용전체 레코드가 새 키 사용

리키 패스는 quipu-server rekey config.json 커맨드(또는 임베디드 AuditStore::rekey())로 실행합니다. 오프라인 전용입니다 — 서버가 내린 상태에서만 실행할 수 있습니다(스토어 락이 동시 실행을 차단).

RSA 리키의 실제 동작 — rewrap

RSA 필드 리키는 "데이터 전체를 다시 암호화"가 아닙니다. AES 암호문(ciphertext)과 nonce는 그대로 두고, DEK(AES 키)를 감싼 RSA 래핑만 교체합니다. 이것이 KeyRing::rewrap()이 하는 일입니다.

crates/quipu-core/src/crypto.rs — KeyRing::rewrappub fn rewrap(&self, stored: &StoredValue) -> Result<StoredValue> {
    // 1. 구버전 개인키로 DEK 복호화 (unwrap)
    let dek = old.decrypt(Oaep::new::<Sha256>(), &b64::decode(wrapped_key))?;
    // 2. 활성 버전 공개키로 DEK 재암호화 (re-wrap)
    let wrapped = self.rsa_public_of(active)?.encrypt(&mut rand::thread_rng(),
        Oaep::new::<Sha256>(), &dek)?;
    // nonce와 ciphertext는 그대로 — DEK 자체는 안 바뀜, 페이로드 무결성 유지
    Ok(StoredValue::Rsa { key_version: active, wrapped_key: b64::encode(&wrapped),
        nonce: nonce.clone(), ciphertext: ciphertext.clone() })
}

AES 암호문을 건드리지 않으므로 리키 중 전원이 꺼져도 페이로드가 손상되지 않습니다. DEK 재포장만 실패하는 것이고, 이미 재포장된 레코드는 새 키로 읽을 수 있습니다.

리키의 무결성 문제 — RekeyEvent

리키는 레지스트리 테이블 전체를 새로 씁니다. 머클 트리 관점에서 이것은 변조와 구분이 안 됩니다 — 나무가 통째로 바뀌니까요. 이 "정당한 재작성"을 "부정한 변조"와 구분하는 장치가 RekeyEvent입니다.

crates/quipu-core/src/store.rs — RekeyEventpub struct RekeyEvent {
    pub occurred_at: u64,
    pub rsa_version: KeyVersion,
    pub hmac_version: KeyVersion,
    pub tables: Vec<RekeyedTable>,    // 테이블별 old_root → new_root 전환
    pub signing_key_version: KeyVersion,
    pub signature: Vec<u8>,           // RSA 서명: 재작성이 감사됐음을 증명
}

리키 패스가 완료되면 이 이벤트가 meta 테이블에 기록됩니다. verify_integrity()가 이 서명을 검증하고, 각 레지스트리의 현재 트리가 서명된 new_root와 일관성 있는지 확인합니다(일관성 증명). "감사된 재작성"과 "조용한 변조"가 구분됩니다.

HMAC 필드는 리키가 불가능하다

중요한 제약이 하나 있습니다. HMAC과 SHA-256으로 보호된 필드는 리키할 수 없습니다. 일방향 함수이기 때문입니다 — 다이제스트에서 원본 평문을 복구할 방법이 없고, 평문이 없으면 새 키로 새 다이제스트를 만들 수 없습니다.

주의

HMAC 키가 유출되면 유출 이전에 기록된 다이제스트는 영구적으로 노출된 것입니다. 공격자가 그 키와 다이제스트를 갖고 사전 공격을 할 수 있습니다. HMAC 키는 유출되지 않도록 매우 엄중히 관리해야 하고, 정기 로테이션으로 유출 후 피해 기간을 최소화해야 합니다. "유출되면 되돌릴 수 있다"는 안전망이 없습니다.

키 유실 = 데이터 영구 소실

Quipu-Log에는 키 에스크로, 마스터 키, 복구 백도어가 없습니다. 이것은 의도된 설계입니다 — 내부자 위협을 포함한 더 넓은 위협 모델을 막기 위해 백도어를 없앴습니다. 그 대가로, RSA 개인키를 잃으면 그 키로 보호된 모든 레코드는 영영 복호화 불가입니다.

보안 포인트

키를 스토어 디렉토리와 같은 장소에 백업하지 마세요. 스토어 백업을 탈취한 공격자가 같은 장소의 키 백업도 가져가면 암호화 보호가 무너집니다. 키는 스토어와 분리된 별도 경로로 백업하고, HSM(하드웨어 보안 모듈)이나 KMS 사용을 고려하세요.

서명 키 격리

체크포인트 서명에 쓰는 RSA 개인키는 데이터 암호화에 쓰는 키와 분리하는 것을 권장합니다. 서명 키가 데이터 노드(스토어가 돌아가는 서버)에 있으면, 디스크 접근 권한을 가진 내부자가 서명 키를 이용해 체크포인트를 위조할 수 있습니다. 서명 키를 별도 호스트나 HSM에 두면 이 공격이 불가능해집니다.

정리

  • KeyRing: 버전 번호(u32)로 RSA+HMAC 키를 관리. 최고 버전 = 활성 키, 낮은 버전 = 읽기용.
  • 로테이션(평시): 새 버전 키 추가만. 기존 레코드 건드리지 않음. 저렴하고 무중단.
  • 리키(유출 후): DEK 재포장으로 RSA 필드 재암호화. 오프라인, 비쌈. RekeyEvent로 감사됨.
  • HMAC 필드는 리키 불가 — 일방향이라 원본 복구 불가. 유출 시 기존 다이제스트 영구 노출.
  • 키 유실 = 복구 불가 — 에스크로 없음. 키 백업은 스토어와 반드시 분리.
스스로 확인

① 키 로테이션과 리키(re-key)의 차이를 "언제", "하는 일", "비용" 세 가지로 설명해 보세요.
② HMAC 보호 필드가 리키 불가능한 수학적 이유를 설명해 보세요.
③ RSA 개인키를 스토어 백업과 같은 경로에 저장하면 왜 위험한가요?