본문 바로가기
Backend2026년 9월 18일10분 읽기

익명 자격 증명 설계 — 신원을 저장하지 않고 권한만 검증하는 법

YS
김영삼
조회 220
익명 자격 증명 설계 — 신원을 저장하지 않고 권한만 검증하는 법

많은 기능이 "이 사람이 자격이 있는가"만 알면 되는데, 구현은 대개 "이 사람이 누구인가"를 저장하는 방식으로 한다. 익명 자격 증명은 이 둘을 분리한다. 권한은 검증하되 식별자는 남기지 않는다.

적용 대상은 생각보다 많다. 익명 설문의 1인 1표, 무료 체험 남용 방지, 유료 회원 전용 콘텐츠, 신고 기능의 중복 방지, 봇 차단. 전부 "자격 확인"이지 "신원 확인"이 아니다.

이 접근을 처음 검토한 건 익명 피드백 기능을 만들 때였다. 익명이라고 해 놓고 서버에는 사용자 ID를 저장하고 있었다. 중복 제출을 막아야 했으니까. 그런데 그건 익명이 아니다. 관리자가 조회하면 누가 썼는지 다 보인다. 진짜 익명이면서 중복도 막으려면 다른 구조가 필요하다.

무엇을 증명할 것인가

요구저장하면 안 되는 것증명할 것
1인 1회 참여참여자 목록"미사용 토큰을 하나 가지고 있다"
유료 회원 확인어떤 회원이 접근했는지"유효한 회원 자격 증명을 보유한다"
성인 여부 확인생년월일"기준 연령 이상이다"
중복 신고 방지신고자 신원"이 대상에 대해 아직 신고하지 않았다"
봇 차단장치 지문"비용을 지불했거나 챌린지를 통과했다"
설계의 출발점 요구사항을 "누가"가 아니라 "무엇이 참인가"로 다시 쓰는 것. 이 문장 변환만 해도 절반은 끝난다. 대부분의 개인정보 수집은 요구사항을 신원 기반으로 잘못 표현한 데서 시작한다.

가장 실용적인 수단 — 블라인드 서명

복잡한 증명 시스템을 쓰지 않고도 익명성과 검증을 동시에 얻을 수 있는 고전적 방법이 블라인드 서명이다. 개념은 단순하다. 사용자가 토큰을 봉투에 넣어 서버에 주고, 서버는 내용을 보지 않고 봉투째 서명한다. 사용자는 봉투를 벗겨 서명된 토큰을 얻는다.

1
발급 단계
사용자가 로그인 상태로 토큰 요청. 서버는 "이 사용자는 자격이 있다"를 확인하고 블라인드 서명을 발급한다. 이때 서버는 토큰의 실제 값을 모른다.
2
사용 단계
사용자가 서명된 토큰을 제시한다. 이때는 로그인하지 않는다(또는 다른 세션).
3
검증
서버는 서명이 유효한지, 그리고 이 토큰이 아직 사용되지 않았는지 확인한다.
4
기록
사용된 토큰의 식별자만 저장한다. 그 토큰이 누구에게 발급됐는지는 어디에도 없다.
// 흐름 요약 (라이브러리 사용을 전제로 한 개념 코드)
// 1) 클라이언트: 무작위 토큰 생성 후 블라인딩
const token = randomBytes(32);
const { blinded, unblinder } = blind(token, serverPublicKey);

// 2) 서버: 자격 확인 후 블라인드 서명 (token 값은 모름)
if (!user.isEligible()) throw new Error('not eligible');
if (await hasIssued(user.id)) throw new Error('already issued');
await markIssued(user.id);                 // "발급했다"만 기록
const blindSig = signBlinded(blinded, serverPrivateKey);

// 3) 클라이언트: 언블라인딩 → 서명된 토큰 획득
const signature = unblind(blindSig, unblinder);

// 4) 사용 시점 (다른 세션, 로그인 불필요)
//    서버는 서명 유효성과 미사용 여부만 확인
if (!verify(token, signature, serverPublicKey)) reject();
if (await isSpent(hash(token))) reject();
await markSpent(hash(token));              // 사용된 토큰 해시만 저장

서버가 가진 정보는 두 가지뿐이다. "사용자 A에게 발급했다"와 "토큰 해시 X가 사용됐다". 이 둘을 연결할 방법이 없다. 이것이 익명성의 근거다.

연결 가능성(linkability)이라는 함정

암호학적으로 완벽해도 운영에서 연결이 드러나는 경우가 많다. 실무에서 더 자주 문제가 되는 건 이쪽이다.

연결 가능성 점검
시간 — 발급 직후 바로 사용하면 시각으로 추정된다. 사용 시점을 분산시키거나 지연을 권장
IP 주소 — 발급과 사용이 같은 IP면 연결된다. 사용 요청의 IP를 저장하지 않거나 해시만 보관
세션·쿠키 — 같은 세션에서 두 단계를 수행하면 무의미해진다
사용자 수 — 익명 집합이 작으면 추론이 쉽다. 참여자가 3명인 익명 설문은 익명이 아니다
로그 — 애플리케이션 로그, 액세스 로그, 에러 추적 도구에 무엇이 남는가
부가 데이터 — 제출 내용 자체에 신원이 드러나는 정보가 섞이지 않는가
익명 집합의 크기 익명성은 "나를 구분할 수 없는 사람이 몇 명인가"로 결정된다. 기술이 아무리 좋아도 그 수가 작으면 소용없다. 참여자가 적은 기능에서는 애초에 익명을 약속하지 않는 편이 정직하다.

비용 기반 증명 — 스팸에 대한 다른 접근

자격을 부여할 근거가 없는 완전 익명 서비스에서는 비용을 부과하는 방식이 쓰인다. 계정 생성이나 요청에 한계비용을 만들어 대량 생성을 억제하는 것이다.

비용 형태장점단점
소액 결제계산이 명확, 효과가 크다결제 수단 자체가 신원 근접 정보, 접근성 저하
작업 증명신원 불필요저사양 기기에 불리, 공격자는 비용을 감당할 수 있음
챌린지(캡차 등)구현이 쉽다접근성 문제, 자동 해결 비용 하락
초대·평판품질이 높다폐쇄적, 초기 확산 어려움

결제를 쓴다면 결제와 계정을 분리하는 장치가 필요하다. 그렇지 않으면 익명성이 결제 사업자 쪽으로 옮겨 갈 뿐이다. 앞서 본 블라인드 서명 구조를 결제 영수증에 적용하면 이 분리가 가능해진다.

직접 구현하지 말아야 할 것들

암호 프리미티브는 직접 구현하면 거의 반드시 틀린다. 특히 블라인드 서명과 영지식 증명은 미묘한 구현 오류가 보안을 통째로 무너뜨린다.

  • 검증된 라이브러리를 사용하고, 버전과 취약점 공지를 추적한다.
  • 키 관리 계획을 먼저 세운다 — 서명 키가 유출되면 토큰을 무한 생성할 수 있다.
  • 키 회전 절차를 설계에 포함한다. 회전 시 기존 토큰의 유효 기간을 어떻게 할 것인가.
  • 재생 공격 방지를 반드시 포함한다 — 사용된 토큰 기록은 빠르고 확실해야 한다(경쟁 조건 주의).
  • 만료를 넣는다. 영원히 유효한 토큰은 운영상 위험하다.
-- 사용된 토큰 기록은 경쟁 조건에 강해야 한다
CREATE TABLE spent_tokens (
  token_hash bytea PRIMARY KEY,        -- 유니크 제약이 곧 중복 방지
  spent_at   timestamptz NOT NULL DEFAULT now(),
  epoch      int NOT NULL              -- 키 회전 세대
);

-- 삽입이 성공하면 사용 처리, 충돌하면 이미 사용된 토큰
INSERT INTO spent_tokens(token_hash, epoch) VALUES ($1, $2)
ON CONFLICT (token_hash) DO NOTHING
RETURNING token_hash;
-- 반환이 없으면 재사용 시도 → 거부

-- 만료된 세대는 주기적으로 정리
DELETE FROM spent_tokens WHERE epoch < $current_epoch - 2;

언제 이 복잡성을 감수할 가치가 있나

도입할 만한 경우
  • 프라이버시가 제품의 핵심 약속인 서비스
  • 내부 조회로 신원이 드러나면 안 되는 기능(제보·신고·설문)
  • 규제상 개인정보 최소 수집이 요구되는 영역
  • 신뢰가 곧 제품 가치인 경우
과한 경우
  • 사용자가 이미 로그인해 있고 그 사실이 문제되지 않는 기능
  • 참여자 수가 적어 익명 집합이 작은 경우
  • 운영·감사 요구로 어차피 추적이 필요한 경우
  • 단순 중복 방지면 충분한 경우

마지막 판단 기준은 이것이다. "우리 회사 직원이 DB를 열어 봤을 때 누가 무엇을 했는지 알 수 있어야 하는가?" 알아서는 안 된다면 익명 자격 증명이 필요하고, 알아도 된다면 일반적인 인증으로 충분하다.

자주 묻는 질문

익명 자격 증명이 정확히 무엇인가요?

사용자의 신원을 저장하지 않고 특정 자격만 검증하는 인증 방식입니다. "누구인가" 대신 "유효한 자격이 있는가"만 확인하며, 발급 기록과 사용 기록이 서로 연결되지 않도록 설계합니다.

블라인드 서명은 어떻게 익명성을 보장하나요?

서버가 토큰의 실제 값을 보지 못한 상태로 서명하기 때문입니다. 나중에 토큰이 제시돼도 서버는 그것이 누구에게 발급한 서명인지 알 수 없으며, 발급 기록과 사용 기록 사이에 연결고리가 없습니다.

암호 기술만 잘 쓰면 익명성이 보장되나요?

아닙니다. 발급과 사용의 시간 간격, IP 주소, 세션 정보, 로그, 그리고 익명 집합의 크기가 실제 익명성을 좌우합니다. 참여자가 소수라면 어떤 기술을 써도 추론이 가능하므로 설계 단계에서 이를 고려해야 합니다.

영지식 증명을 직접 구현해야 하나요?

권하지 않습니다. 대부분의 요구는 블라인드 서명이나 일회성 토큰처럼 더 단순한 수단으로 충분히 달성됩니다. 어떤 경우에도 암호 프리미티브는 검증된 라이브러리를 사용하고 키 관리와 회전 절차를 먼저 설계하세요.

토큰 재사용은 어떻게 막나요?

사용된 토큰의 해시를 유니크 제약이 있는 테이블에 기록하고, 삽입 성공 여부로 사용 가능 여부를 판단하면 경쟁 조건에 안전합니다. 토큰에 만료와 키 세대를 두어 오래된 기록을 정리할 수 있게 설계하세요.

결제로 스팸을 막으면 익명성이 깨지지 않나요?

결제와 계정을 직접 연결하면 그렇습니다. 결제 영수증에 블라인드 서명 구조를 적용해 "유효한 결제가 있었다"는 사실만 증명하고 어떤 계정과 연결되는지는 서버가 알 수 없게 만들면 분리가 가능합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록