이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 암호화, 서명, MAC, hashing은 각각 어떤 보안 속성을 제공하는가?
- 직접 crypto protocol을 만들 때 자주 나오는 실수는 무엇인가?
- Java/Spring 애플리케이션에서 키 분리, nonce/IV, AEAD, 서명 검증 실패를 어떻게 다뤄야 하는가?
개요
암호화는 주로 기밀성을 제공한다. 서명은 무결성과 서명자의 출처 확인을 제공한다. MAC은 공유 secret을 가진 주체 사이에서 무결성과 출처 확인을 제공한다. hash는 데이터 fingerprint를 만들 수 있지만 secret 없이 사용하면 인증을 제공하지 않는다.
이 차이를 섞으면 취약한 설계가 나온다. 예를 들어 base64 encoding을 암호화라고 부르거나, 암호화만 하고 변조 방지를 하지 않거나, 서명된 JWT는 읽히지 않는다고 오해하는 식이다.
현대 애플리케이션에서는 가능하면 검증된 라이브러리와 표준 프로토콜을 사용해야 한다. 직접 알고리즘을 조합하거나 nonce/IV를 재사용하거나 같은 key를 여러 목적에 쓰면 작은 실수가 전체 경계를 무너뜨린다.
이 문서의 핵심은 “암호를 썼다”가 아니라 “어떤 보안 속성을 어떤 key와 어떤 lifecycle로 얻는가”다.
공격 시나리오
- 공격자는 암호문을 수정해 서버가 복호화 후 다른 권한이나 금액으로 해석하게 만든다.
- 같은 nonce/IV가 반복되면 암호문 사이의 관계를 분석한다.
- JWT나 쿠키가 서명만 되어 있는데 민감 정보가 들어 있으면 payload를 그대로 읽는다.
- HMAC 검증이 없거나 실패를 무시하면 요청 parameter를 조작한다.
- 같은 key를 암호화, 서명, token 발급에 모두 사용하면 한 경계 유출이 모든 기능으로 번진다.
- base64, URL encoding, hashing을 암호화로 착각한 저장값을 쉽게 복원하거나 대입한다.
취약한 요청/응답 예시
String payload = userId + ":" + role + ":" + expiresAt;
String token = Base64.getEncoder().encodeToString(payload.getBytes(UTF_8));
response.addCookie(new Cookie("APP_TOKEN", token));Cookie: APP_TOKEN=MTAwNDpBRE1JTjoyMDI2LTA3LTAxVDEyOjAwOjAwbase64는 encoding일 뿐 암호화나 서명이 아니다. 공격자는 값을 읽고 바꾼 뒤 다시 encoding할 수 있다.
주제별 핵심 판단
- encoding은 표현 변환이고 보안 경계가 아니다.
- hash는 secret 없이 쓰면 무결성 인증을 제공하지 않는다.
- HMAC은 공유 secret 기반 무결성 확인에 적합하다.
- digital signature는 private key로 서명하고 public key로 검증해 비대칭 출처 확인을 제공한다.
- 암호화만으로 변조 방지가 자동 제공되지 않는 모드가 있다.
- 가능한 AEAD, 예를 들어 AES-GCM이나 ChaCha20-Poly1305처럼 인증된 암호화를 사용한다.
- key는 목적별로 분리하고 version/kid로 rotation할 수 있어야 한다.
개선된 요청/응답 예시
public record SignedMessage(
String keyId,
String payload,
String signature
) {}public SignedMessage sign(String payload) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(keyResolver.currentHmacKey());
String signature = base64Url(mac.doFinal(payload.getBytes(UTF_8)));
return new SignedMessage(keyResolver.currentKeyId(), payload, signature);
}public void verify(SignedMessage message) {
Key key = keyResolver.resolve(message.keyId());
byte[] expected = hmac(key, message.payload());
if (!MessageDigest.isEqual(expected, base64UrlDecode(message.signature()))) {
throw new InvalidSignatureException();
}
}서명 검증은 constant-time 비교를 사용하고 실패 시 payload를 신뢰하지 않는다. 민감한 payload라면 서명이 아니라 암호화 또는 AEAD가 필요하다.
방어 설계
- 보호하려는 속성을 먼저 정한다. 기밀성, 무결성, 출처 확인, 재생 방지 중 무엇이 필요한지 분리한다.
- 민감 데이터를 저장해야 하면 데이터 분류와 보존 필요성을 먼저 검토한다.
- 암호화가 필요하면 검증된 라이브러리의 AEAD 모드를 우선 사용한다.
- nonce/IV는 모드 요구사항에 맞게 생성하고 재사용하지 않는다.
- key는 목적, 환경, tenant, data class에 따라 분리한다.
- key id, 생성 시각, 활성/검증/폐기 상태를 metadata로 관리한다.
- 서명 검증 실패는 인증 실패나 보안 이벤트로 처리하고 fallback으로 통과시키지 않는다.
- crypto error message는 내부 key id와 알고리즘 상세를 클라이언트에 노출하지 않는다.
Spring/HTTP 예시
@PostMapping("/webhooks/payment")
public ResponseEntity<Void> receive(@RequestHeader("X-Signature") String signature,
@RequestBody byte[] body) {
if (!webhookVerifier.verify(body, signature)) {
audit.warn("webhook_signature_invalid");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
paymentWebhookService.handle(body);
return ResponseEntity.noContent().build();
}level=WARN event=signature_validation_failed result=blocked source=payment-webhook keyId=k2026-07 reason=hmac_mismatch traceId=91c78a20서명 검증은 body parsing과 업무 처리보다 먼저 일어나야 한다. 검증 실패 payload는 신뢰할 수 없는 입력으로 남긴다.
운영 로그와 감사 지점
level=WARN event=crypto_policy_violation result=blocked artifact=session-cookie reason=unsigned_token_detected traceId=91c78a20level=INFO event=crypto_key_rotated result=allowed purpose=webhook-hmac oldKeyId=k2026-01 newKeyId=k2026-07 overlapDays=7 traceId=91c78a20- key 원문, plaintext, full ciphertext는 로그에 남기지 않는다.
- crypto 실패는
decrypt_failed,signature_invalid,unknown_key_id,replayed_nonce처럼 reason을 구분한다. - key rotation 이벤트에는 purpose, old/new key id, overlap 기간을 남긴다.
- nonce 재사용 탐지 가능성이 있는 프로토콜은 nonce id를 저장하거나 replay cache를 둔다.
- crypto library deprecation 경고는 보안 부채로 관리한다.
실전 판단 기준
- “base64로 암호화”라는 말은 거의 항상 오류다.
- 서명된 값은 읽힐 수 있다. 민감 정보를 넣으면 안 된다.
- 암호화된 값도 변조 방지를 따로 확인해야 한다.
- 같은 key를 여러 목적에 쓰면 rotation과 유출 대응이 어려워진다.
- 직접 nonce를 관리하는 모드에서는 재사용이 치명적일 수 있다.
- 예외가 나면 복호화 실패를 무시하고 기본값을 쓰는 fallback은 위험하다.
테스트 포인트
- 서명 payload를 한 글자 바꾸면 검증이 실패하는지 확인한다.
- 알 수 없는 key id로 들어온 token이 거부되는지 확인한다.
- expired 또는 replayed message가 거부되는지 확인한다.
- 암호화된 필드를 bit flip했을 때 복호화 실패로 처리되는지 확인한다.
- 같은 key가 서로 다른 purpose에 재사용되지 않는지 inventory로 확인한다.
- crypto 실패 응답에 key id, algorithm stack trace, plaintext가 없는지 확인한다.
위험 신호!
- encoding을 암호화라고 부른다.
- JWT payload에 민감 정보를 넣고 서명만 믿는다.
- AES-CBC를 쓰면서 MAC이나 인증 태그 검증이 없다.
- nonce/IV가 고정값이거나 재사용된다.
- 하나의
APP_SECRET으로 cookie, JWT, webhook, encryption을 모두 처리한다. - 서명 검증 실패 시 경고만 남기고 업무 처리를 계속한다.
확인 질문
확인 질문
- 암호화와 서명의 차이는 무엇인가?
- 암호화는 주로 기밀성을 제공하고 서명은 무결성과 출처 확인을 제공한다.
- 서명된 값에 민감 정보를 넣으면 왜 위험한가?
- 서명은 읽기를 막지 않으므로 payload는 누구나 decode해서 볼 수 있기 때문이다.
- key separation이 필요한 이유는 무엇인가?
- 한 목적의 key 유출이나 rotation이 다른 목적의 보안 경계를 함께 깨지 않게 하기 위해서다.