패스키 서버는 계정당 한 개만 저장하면 안 된다…등록·복구 설계법

|작성자: QUASA 편집팀|6 분 소요
패스키 서버는 계정당 한 개만 저장하면 안 된다…등록·복구 설계법

패스키 서버에서는 계정당 credential을 한 개만 저장하면 새 인증 수단을 추가할 때 기존 수단을 덮어쓰게 된다. Google의 서버 등록 지침은 한 사용자에게 여러 패스키가 연결될 수 있으므로 공개키 credential을 별도 테이블에 저장하도록 안내한다. 서버가 각 수단을 독립된 행으로 관리해야 분실한 수단만 해제하고 남은 수단으로 로그인할 수 있다.

구현의 중심은 브라우저 API 호출보다 서버의 등록·인증 검증과 계정 복구 정책이다. 등록 때는 새 credential을 검증한 뒤 현재 계정에 묶고, 인증 때는 저장된 공개키로 응답의 서명을 검증한다. 두 절차 모두 서버가 발급한 challenge와 기대한 origin·RP ID를 확인한 뒤에만 계정 상태나 로그인 세션을 바꾼다.

계정 하나에 credential 여러 개를 연결한다

계정에는 서비스 내부 ID와 WebAuthn의 user.id로 사용할 안정적인 식별자를 둔다. user.id는 이메일이나 표시 이름처럼 변경될 수 있는 값과 분리하고, 개인 식별 정보를 담지 않도록 설계하는 편이 좋다. 이름을 바꾸거나 이메일을 교체해도 이미 등록된 credential이 같은 계정에 연결돼 있어야 하기 때문이다.

별도 credential 테이블에는 credential ID를 고유 키로, 계정 ID를 외래 키로 저장한다. 각 행에는 검증된 공개키, signCount, 등록 시각, 마지막 사용 시각, 백업 가능 여부와 현재 백업 상태를 둔다. 사용자가 수단을 구별하도록 표시할 이름을 추가할 수 있고, 인증 화면에 필요하다면 전송 방식도 보관할 수 있다. 개인키는 사용자의 인증 수단에 남으며 서버의 저장 대상이 아니다.

고유 제약은 계정 ID가 아니라 credential ID에 적용한다. 그래야 한 계정이 여러 수단을 보유하면서도 같은 credential ID가 다른 계정에 다시 등록되는 일을 막을 수 있다. 등록 응답에 실린 계정 식별자를 신뢰해 연결 대상을 정하지 말고, 인증된 세션이나 확정된 가입 절차가 가리키는 계정에 새 행을 넣는다.

예를 들어 동기화되는 패스키와 별도 보안 키를 가진 사용자는 같은 계정으로 로그인하지만 서버에는 서로 다른 credential 행을 가진다. 한 수단을 잃어도 다른 행의 공개키와 인증 권한은 유지된다. 관리 화면에서 수단별 이름과 등록·마지막 사용 시각을 보여주면 사용자가 해제 대상을 구별하기 쉽다.

등록은 challenge 발급부터 저장까지 한 절차로 처리한다

기존 휴대전화 패스키와 별도로 FIDO 보안 키를 등록해 인증 수단을 추가하는 상황

서버는 등록 시작 시 예측하기 어려운 새 challenge를 만들고 해당 요청을 계정과 세션에 결부한다. 클라이언트에 전달할 생성 옵션에는 RP ID, 안정적인 user.id, 허용할 공개키 알고리즘과 사용자 확인 정책을 담는다. 이미 등록된 credential ID를 excludeCredentials에 넣으면 같은 수단의 중복 등록을 줄일 수 있다.

브라우저가 navigator.credentials.create로 받은 응답을 서버에 보내면, 서버는 저장 전에 clientDataJSON과 attestationObject를 해석한다. 응답의 절차 유형, challenge, 허용한 origin, RP ID 해시, 요구한 사용자 확인 조건과 공개키 알고리즘을 검증해야 한다. 별도의 인증기 증명서 검증을 요구하지 않는 정책이라도 challenge 일치 확인은 생략하지 않는다.

검증이 끝난 뒤 credential ID가 이미 등록됐는지 확인하고 공개키와 상태값을 저장한다. 동시에 진행된 다른 요청이 같은 ID를 먼저 저장할 수 있으므로 조회 결과와 별개로 데이터베이스의 고유 제약이 필요하다. 중복이 발견되면 기존 행을 다른 계정으로 옮기거나 덮어쓰지 않고 이번 등록을 실패 처리한다.

등록 challenge는 만료 시간을 두고 절차가 끝나면 성공 여부와 관계없이 폐기한다. 사용자가 등록을 다시 시작하면 새 요청과 새 challenge를 발급해야 이전 응답이 뒤늦게 도착해도 혼동하지 않는다. 여러 등록 창을 동시에 허용한다면 세션의 단일 challenge 필드를 덮어쓰는 대신 요청별 식별자와 상태를 관리한다.

새 기기 추가와 계정 복구 뒤 재등록에도 같은 응답 검증을 적용한다. 복구 세션이 새 credential을 추가할 권한을 줄 수는 있지만, 그 세션 자체가 새 공개키의 유효성을 증명하지는 않는다. 패스키 생성은 완료됐으나 서버 저장이 실패한 경우에는 재시도 시 중복 ID를 명확하게 처리하고, 사용자에게 등록 완료로 표시하기 전에 서버 저장 결과를 확인한다.

인증은 credential 조회와 서명 검증이 끝난 뒤 승인한다

로그인 시도마다 서버는 새 challenge를 발급해 인증 요청에 연결하고, 응답 처리 후 성공·실패와 관계없이 폐기한다. Google의 서버 인증 지침은 만료된 challenge도 버리고 응답의 credential ID로 저장된 공개키를 찾도록 안내한다. 재사용된 challenge나 이미 처리된 응답으로 로그인 세션을 다시 만들 수 없어야 한다.

사용자가 계정을 먼저 지정했다면 응답의 credential ID가 그 계정에 속하는지 확인한다. 계정을 먼저 지정하지 않는 로그인에서는 credential ID로 행을 조회해 연결된 계정을 찾고, 응답의 userHandle도 저장된 계정 식별자와 대조한다. 클라이언트가 함께 보낸 이메일을 인증 대상 계정을 결정하는 근거로 삼아서는 안 된다.

조회한 credential이 삭제됐거나 비활성화된 상태라면 서명이 맞더라도 로그인에 사용하지 않는다. 허용 credential 목록을 지정한 요청에서는 반환된 ID가 그 목록에 있는지도 확인한다. 이 검사는 수학적 서명 검증과 별도로, 해당 수단에 현재 계정 접근 권한이 있는지를 판단한다.

서버는 응답의 절차 유형과 challenge, 기대한 origin, RP ID 해시를 확인한다. 사용자 존재를 뜻하는 UP 플래그와 정책상 요구한 사용자 확인인 UV 플래그를 검사한 뒤 저장된 공개키로 서명을 검증한다. 서명 대상은 challenge 문자열 자체가 아니라 authenticatorData와 clientDataJSON 해시를 이어 붙인 데이터다. 모든 검증을 통과한 뒤 로그인 세션을 발급한다.

동시에 열린 인증 시도를 허용한다면 각 challenge의 만료와 소비 상태를 별도로 기록한다. 응답 처리 과정에서 challenge를 원자적으로 소비해야 같은 요청이 거의 동시에 두 번 승인되는 일을 막을 수 있다. 검증에 실패한 응답도 해당 시도의 challenge를 끝내고, 사용자가 계속하려면 새 로그인 요청을 시작하게 한다.

카운터와 백업 상태는 credential마다 기록한다

W3C WebAuthn 규격은 signCount를 인증기 복제 가능성을 살피는 신호로 설명하고, 백업 가능 여부인 BE와 현재 백업 상태인 BS를 구별한다. BE는 credential 생성 때 정해져 유지되지만 BS는 이후 바뀔 수 있다. 서버에서 두 값을 하나의 ‘백업됨’ 표시로 합치면 현재 상태와 백업 가능성을 혼동하게 된다.

등록 때 받은 signCount를 해당 credential 행에 저장하고, 인증 때 반환된 값과 비교한다. 이전 값과 새 값 중 하나라도 0이 아닌데 새 값이 이전 값 이하라면 조사할 신호다. 일부 인증기는 카운터를 계속 0으로 두며, 값이 증가하지 않는 원인에는 인증기 오작동이나 응답 처리 순서의 차이도 있다. 이 값만으로 복제를 확정해 모든 수단을 일괄 차단하기보다 서비스 위험도에 맞는 재확인 절차를 정한다.

BE가 켜져 있어도 BS가 꺼져 있으면 그 credential이 현재 백업됐다고 판단할 수 없다. 인증 응답에서 상태 변화를 확인하고, 정책에 사용한다면 서버의 BS 기록을 갱신한다. 단일 기기에만 있는 수단은 그 기기를 잃었을 때 사용할 수 없으므로 다른 인증 수단을 추가하거나 별도의 복구 경로를 마련해야 한다.

마지막 사용 시각은 인증 검증이 끝난 뒤 갱신한다. 이 값은 사용자가 오래 쓰지 않은 수단을 찾아 관리하는 데 도움이 되지만, 오래됐다는 이유만으로 자동 삭제하면 남아 있던 복구 수단까지 없앨 수 있다. 카운터와 백업 상태, 사용 시각은 각기 다른 목적의 서버 기록이며 어느 것도 공개키 서명 검증을 대신하지 않는다.

분실·복구·삭제는 남은 인증 수단을 기준으로 설계한다

휴대전화를 잃은 뒤에도 남아 있는 FIDO 보안 키로 계정 접근을 이어가는 상황

기기를 잃었어도 다른 등록 수단이 남아 있다면 그 수단으로 로그인한 뒤 분실한 credential만 해제하게 한다. 이어 새 패스키를 등록하면 계정 접근을 유지하면서 분실 수단의 인증 권한을 없앨 수 있다. 관리 화면에는 어떤 credential을 없애는지 구별할 정보가 필요하고, 삭제 요청 자체에도 적절한 재확인을 적용하는 편이 안전하다.

삭제하려는 항목이 사용자의 유일한 인증 수단이라면 대체 수단 등록이나 복구 경로를 먼저 확인해야 한다. 서버에서 credential 행을 비활성화하거나 제거하면 이후 그 ID의 인증 응답을 거부할 수 있다. 서버의 권한 해제와 사용자의 기기 또는 동기화 서비스에 저장된 패스키 정리는 별도 작업이므로, 사용자에게 두 작업의 범위를 분명히 알려야 한다.

모든 패스키에 접근할 수 없는 경우에는 기존 credential의 서명으로 본인을 확인할 수 없다. 서비스가 마련한 별도 복구 수단으로 계정 소유를 확인한 뒤 권한이 제한된 복구 세션에서 새 credential을 등록하도록 설계한다. 복구가 끝났다는 이유만으로 이전 수단을 모두 자동 삭제하기보다, 실제로 분실되거나 노출된 항목을 확인해 개별적으로 해제한다.

복구 경로가 새 인증 수단을 등록할 권한을 주는 만큼 신원 확인 강도는 계정의 위험도에 맞춰 정해야 한다. 삭제·재등록 기록을 계정 활동에 남기면 사용자가 어떤 수단이 바뀌었는지 확인할 수 있다. 서버 구현에서는 여러 수단이 남은 계정, 유일한 수단을 잃은 계정, 해제된 credential로 다시 로그인하는 경우를 각각 구분해 처리한다.

함께 읽기:

공유:

뉴스레터 구독

최신 Web3, AI, 암호화폐 뉴스를 이메일로 받아보세요.

0